├── examples ├── src │ ├── .npmignore │ ├── favicon.ico │ ├── .gitignore │ ├── data │ │ ├── users.js │ │ └── states.js │ ├── components │ │ ├── CustomSingleValue.js │ │ ├── UsersField.js │ │ ├── CustomOption.js │ │ ├── SelectedValuesField.js │ │ ├── DisabledUpsellOptions.js │ │ ├── CustomRenderField.js │ │ ├── RemoteSelectField.js │ │ ├── MultiSelectField.js │ │ ├── StatesField.js │ │ └── ValuesAsNumbersField.js │ ├── standalone.html │ ├── app.js │ ├── index.html │ └── example.less └── dist │ ├── .gitignore │ ├── standalone.html │ ├── index.html │ └── example.css ├── less ├── default.less ├── spinner.less ├── mixins.less ├── menu.less ├── select.less ├── multi.less └── control.less ├── .npmignore ├── .coveralls.yml ├── .eslintignore ├── .travis.yml ├── .gitignore ├── testHelpers └── jsdomHelper.js ├── .editorconfig ├── wallaby.js ├── gulpfile.js ├── src ├── SingleValue.js ├── Option.js ├── Value.js └── Select.js ├── lib ├── SingleValue.js ├── Option.js ├── Value.js └── Select.js ├── bower.json ├── .eslintrc ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── test └── Value-test.js ├── dist ├── default.css └── react-select.min.js ├── README.md └── HISTORY.md /examples/src/.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /less/default.less: -------------------------------------------------------------------------------- 1 | @import "select.less"; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | bower.json 3 | examples/dist 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service-name: travis-ci 2 | repo_token: itdMRdBNgDK8Gb5nIA63zVMEryaxTQxkR 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/* 2 | dist/* 3 | examples/dist/* 4 | node_modules/* 5 | bower_components/* 6 | -------------------------------------------------------------------------------- /examples/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/react-select/master/examples/src/favicon.ico -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | - "0.11" 6 | - "0.12" 7 | - "iojs-v2.5.0" 8 | script: 9 | - npm run coveralls 10 | -------------------------------------------------------------------------------- /examples/dist/.gitignore: -------------------------------------------------------------------------------- 1 | ## This file is here to ensure it is included in the gh-pages branch, 2 | ## when `gulp deploy` is used to push updates to the demo site. 3 | 4 | # Dependency directory 5 | node_modules 6 | -------------------------------------------------------------------------------- /examples/src/.gitignore: -------------------------------------------------------------------------------- 1 | ## This file is here to ensure it is included in the gh-pages branch, 2 | ## when `gulp deploy` is used to push updates to the demo site. 3 | 4 | # Dependency directory 5 | node_modules 6 | -------------------------------------------------------------------------------- /examples/src/data/users.js: -------------------------------------------------------------------------------- 1 | exports.users = [ 2 | { value: 'John Smith', label: 'John Smith', email: 'john@smith.com' }, 3 | { value: 'Merry Jane', label: 'Merry Jane', email: 'merry@jane.com' }, 4 | { value: 'Stan Hoper', label: 'Stan Hoper', email: 'stan@hoper.com' } 5 | ]; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage tools 11 | lib-cov 12 | coverage 13 | 14 | # Dependency directory 15 | node_modules 16 | bower_components 17 | 18 | # Publish directory 19 | .publish 20 | 21 | # Other 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /testHelpers/jsdomHelper.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (html) { 3 | if (typeof document !== 'undefined') { 4 | return; 5 | } 6 | 7 | var jsdom = require('jsdom').jsdom; 8 | global.document = jsdom(html || ''); 9 | global.window = global.document.parentWindow; 10 | global.navigator = { 11 | userAgent: 'JSDOM' 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = tab 11 | 12 | [*.json] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.yml] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is the config file for the [Wallabyjs](http://wallabyjs.com) test runner 3 | */ 4 | 5 | var babel = require('babel'); 6 | 7 | module.exports = function (wallaby) { // eslint-disable-line no-unused-vars 8 | return { 9 | files: ['src/*.js', 'testHelpers/*.js'], 10 | tests: ['test/*-test.js' ], 11 | env: { 12 | type: 'node', 13 | runner: 'node' 14 | }, 15 | preprocessors: { 16 | '**/*.js': file => babel.transform(file.content, { sourceMap: true }) 17 | } 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | initGulpTasks = require('react-component-gulp-tasks'); 3 | 4 | var taskConfig = { 5 | 6 | component: { 7 | name: 'Select', 8 | less: { 9 | path: 'less', 10 | entry: 'default.less' 11 | } 12 | }, 13 | 14 | example: { 15 | src: 'examples/src', 16 | dist: 'examples/dist', 17 | standalone: true, 18 | files: [ 19 | 'index.html', 20 | 'standalone.html', 21 | '.gitignore' 22 | ], 23 | scripts: [ 24 | 'app.js' 25 | ], 26 | less: [ 27 | 'example.less' 28 | ] 29 | } 30 | 31 | }; 32 | 33 | initGulpTasks(gulp, taskConfig); 34 | -------------------------------------------------------------------------------- /less/spinner.less: -------------------------------------------------------------------------------- 1 | // 2 | // Spinner 3 | // ------------------------------ 4 | 5 | .Select-spinner(@size, @orbit, @satellite) { 6 | .animation( Select-animation-spin 400ms infinite linear ); 7 | .square(@size); 8 | box-sizing: border-box; 9 | border-radius: 50%; 10 | border: floor((@size / 8)) solid @orbit; 11 | border-right-color: @satellite; 12 | display: inline-block; 13 | position: relative; 14 | 15 | } 16 | 17 | @keyframes Select-animation-spin { 18 | to { transform: rotate(1turn); } 19 | } 20 | @-webkit-keyframes Select-animation-spin { 21 | to { -webkit-transform: rotate(1turn); } 22 | } 23 | -------------------------------------------------------------------------------- /src/SingleValue.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classes = require('classnames'); 3 | 4 | var SingleValue = React.createClass({ 5 | propTypes: { 6 | placeholder: React.PropTypes.string, // this is default value provided by React-Select based component 7 | value: React.PropTypes.object // selected option 8 | }, 9 | render: function() { 10 | 11 | var classNames = classes('Select-placeholder', this.props.value && this.props.value.className); 12 | return ( 13 |
{this.props.placeholder}
18 | ); 19 | } 20 | }); 21 | 22 | module.exports = SingleValue; 23 | -------------------------------------------------------------------------------- /lib/SingleValue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var classes = require('classnames'); 5 | 6 | var SingleValue = React.createClass({ 7 | displayName: 'SingleValue', 8 | 9 | propTypes: { 10 | placeholder: React.PropTypes.string, // this is default value provided by React-Select based component 11 | value: React.PropTypes.object // selected option 12 | }, 13 | render: function render() { 14 | 15 | var classNames = classes('Select-placeholder', this.props.value && this.props.value.className); 16 | return React.createElement( 17 | 'div', 18 | { 19 | className: classNames, 20 | style: this.props.value && this.props.value.style, 21 | title: this.props.value && this.props.value.title 22 | }, 23 | this.props.placeholder 24 | ); 25 | } 26 | }); 27 | 28 | module.exports = SingleValue; -------------------------------------------------------------------------------- /examples/src/components/CustomSingleValue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Gravatar from 'react-gravatar'; 3 | 4 | var SingleValue = React.createClass({ 5 | propTypes: { 6 | placeholder: React.PropTypes.string, 7 | value: React.PropTypes.object 8 | }, 9 | render () { 10 | var obj = this.props.value; 11 | var size = 15; 12 | var gravatarStyle = { 13 | borderRadius: 3, 14 | display: 'inline-block', 15 | marginRight: 10, 16 | position: 'relative', 17 | top: -2, 18 | verticalAlign: 'middle', 19 | }; 20 | 21 | return ( 22 |
23 | {obj ? ( 24 |
25 | 26 | {obj.value} 27 |
28 | ) : ( 29 | this.props.placeholder 30 | ) 31 | } 32 |
33 | ); 34 | } 35 | }); 36 | 37 | module.exports = SingleValue; 38 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-select", 3 | "main": [ 4 | "dist/react-select.min.js", 5 | "dist/default.css" 6 | ], 7 | "version": "0.6.11", 8 | "homepage": "https://github.com/JedWatson/react-select", 9 | "authors": [ 10 | "Jed Watson" 11 | ], 12 | "description": "A Select control built with and for ReactJS", 13 | "moduleType": [ 14 | "amd", 15 | "globals", 16 | "node" 17 | ], 18 | "dependencies": { 19 | "classnames": "^1.2.2", 20 | "react-input-autosize": "^0.4.3" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "react-component", 25 | "select", 26 | "multiselect", 27 | "combobox", 28 | "input", 29 | "form", 30 | "ui" 31 | ], 32 | "license": "MIT", 33 | "ignore": [ 34 | ".editorconfig", 35 | ".gitignore", 36 | "package.json", 37 | "src", 38 | "node_modules", 39 | "example", 40 | "test" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "rules": { 11 | "curly": [2, "multi-line"], 12 | "no-shadow": 0, 13 | "no-trailing-spaces": 0, 14 | "no-underscore-dangle": 0, 15 | "no-unused-expressions": 0, 16 | "object-curly-spacing": [1, "always"], 17 | "quotes": [2, "single", "avoid-escape"], 18 | "react/jsx-boolean-value": 1, 19 | "react/jsx-no-undef": 1, 20 | "react/jsx-quotes": 1, 21 | "react/jsx-sort-prop-types": 1, 22 | "react/jsx-uses-react": 1, 23 | "react/jsx-uses-vars": 1, 24 | "react/no-did-mount-set-state": 1, 25 | "react/no-did-update-set-state": 1, 26 | "react/no-unknown-property": 1, 27 | "react/prop-types": 1, 28 | "react/react-in-jsx-scope": 1, 29 | "react/self-closing-comp": 1, 30 | "react/sort-comp": 1, 31 | "react/wrap-multilines": 1, 32 | "semi": 2, 33 | "strict": 0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/src/components/UsersField.js: -------------------------------------------------------------------------------- 1 | import GravatarOption from './CustomOption'; 2 | import GravatarValue from './CustomSingleValue'; 3 | import React from 'react'; 4 | import Select from 'react-select'; 5 | 6 | const USERS = require('../data/users'); 7 | 8 | var UsersField = React.createClass({ 9 | propTypes: { 10 | hint: React.PropTypes.string, 11 | label: React.PropTypes.string, 12 | }, 13 | renderHint () { 14 | if (!this.props.hint) return null; 15 | return ( 16 |
{this.props.hint}
17 | ); 18 | }, 19 | render () { 20 | 21 | return ( 22 |
23 |

{this.props.label}

24 | 37 | {this.renderHint()} 38 |
39 | ); 40 | } 41 | }); 42 | 43 | module.exports = SelectedValuesField; -------------------------------------------------------------------------------- /examples/dist/standalone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React-Select Example 4 | 5 | 6 | 7 |
8 |

React Select

9 |

Standalone example

10 |
11 | 12 |
13 | 14 |
15 |
16 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 36 | 37 | -------------------------------------------------------------------------------- /examples/src/standalone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React-Select Example 4 | 5 | 6 | 7 |
8 |

React Select

9 |

Standalone example

10 |
11 | 12 |
13 | 14 |
15 |
16 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 36 | 37 | -------------------------------------------------------------------------------- /less/mixins.less: -------------------------------------------------------------------------------- 1 | // 2 | // Mixins 3 | // ------------------------------ 4 | 5 | 6 | // Utilities 7 | 8 | .size(@width; @height) { 9 | width: @width; 10 | height: @height; 11 | } 12 | .square(@size) { 13 | .size(@size; @size); 14 | } 15 | .border-top-radius(@radius) { 16 | border-top-right-radius: @radius; 17 | border-top-left-radius: @radius; 18 | } 19 | .border-right-radius(@radius) { 20 | border-bottom-right-radius: @radius; 21 | border-top-right-radius: @radius; 22 | } 23 | .border-bottom-radius(@radius) { 24 | border-bottom-right-radius: @radius; 25 | border-bottom-left-radius: @radius; 26 | } 27 | .border-left-radius(@radius) { 28 | border-bottom-left-radius: @radius; 29 | border-top-left-radius: @radius; 30 | } 31 | 32 | 33 | // Vendor Prefixes 34 | 35 | .animation(@animation) { 36 | -webkit-animation: @animation; 37 | -o-animation: @animation; 38 | animation: @animation; 39 | } 40 | .transform(@transform) { 41 | -webkit-transform: @transform; 42 | -moz-transform: @transform; 43 | -ms-transform: @transform; 44 | transform: @transform; 45 | } 46 | .rotate(@degrees) { 47 | -webkit-transform: rotate(@degrees); 48 | -ms-transform: rotate(@degrees); // IE9 only 49 | -o-transform: rotate(@degrees); 50 | transform: rotate(@degrees); 51 | } -------------------------------------------------------------------------------- /examples/src/components/DisabledUpsellOptions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from 'react-select'; 3 | 4 | function logChange() { 5 | console.log.apply(console, [].concat(['Select value changed:'], Array.prototype.slice.apply(arguments))); 6 | } 7 | 8 | var DisabledUpsellOptions = React.createClass({ 9 | displayName: 'DisabledUpsellOptions', 10 | propTypes: { 11 | label: React.PropTypes.string, 12 | }, 13 | onLabelClick: function (data, event) { 14 | console.log(data, event); 15 | }, 16 | renderLink: function() { 17 | return Upgrade here!; 18 | }, 19 | renderOption: function(option) { 20 | return {option.label} {option.link} ; 21 | }, 22 | render: function() { 23 | var ops = [ 24 | { label: 'Basic customer support', value: 'basic' }, 25 | { label: 'Premium customer support', value: 'premium' }, 26 | { label: 'Pro customer support', value: 'pro', disabled: true, link: this.renderLink() }, 27 | ]; 28 | return ( 29 |
30 |

{this.props.label}

31 | 40 |
41 | ); 42 | } 43 | }); 44 | 45 | module.exports = CustomRenderField; -------------------------------------------------------------------------------- /examples/src/components/RemoteSelectField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from 'react-select'; 3 | 4 | var RemoteSelectField = React.createClass({ 5 | displayName: 'RemoteSelectField', 6 | propTypes: { 7 | hint: React.PropTypes.string, 8 | label: React.PropTypes.string, 9 | }, 10 | loadOptions (input, callback) { 11 | input = input.toLowerCase(); 12 | var rtn = { 13 | options: [ 14 | { label: 'One', value: 'one' }, 15 | { label: 'Two', value: 'two' }, 16 | { label: 'Three', value: 'three' } 17 | ], 18 | complete: true 19 | }; 20 | if (input.slice(0, 1) === 'a') { 21 | if (input.slice(0, 2) === 'ab') { 22 | rtn = { 23 | options: [ 24 | { label: 'AB', value: 'ab' }, 25 | { label: 'ABC', value: 'abc' }, 26 | { label: 'ABCD', value: 'abcd' } 27 | ], 28 | complete: true 29 | }; 30 | } else { 31 | rtn = { 32 | options: [ 33 | { label: 'A', value: 'a' }, 34 | { label: 'AA', value: 'aa' }, 35 | { label: 'AB', value: 'ab' } 36 | ], 37 | complete: false 38 | }; 39 | } 40 | } else if (!input.length) { 41 | rtn.complete = false; 42 | } 43 | 44 | setTimeout(function() { 45 | callback(null, rtn); 46 | }, 500); 47 | }, 48 | renderHint () { 49 | if (!this.props.hint) return null; 50 | return ( 51 |
{this.props.hint}
52 | ); 53 | }, 54 | render () { 55 | return ( 56 |
57 |

{this.props.label}

58 | 39 | 40 |
41 | 45 |
46 |
47 | ); 48 | } 49 | }); 50 | 51 | module.exports = MultiSelectField; -------------------------------------------------------------------------------- /less/menu.less: -------------------------------------------------------------------------------- 1 | // 2 | // Select Menu 3 | // ------------------------------ 4 | 5 | 6 | // wrapper around the menu 7 | 8 | .Select-menu-outer { 9 | // Unfortunately, having both border-radius and allows scrolling using overflow defined on the same 10 | // element forces the browser to repaint on scroll. However, if these definitions are split into an 11 | // outer and an inner element, the browser is able to optimize the scrolling behavior and does not 12 | // have to repaint on scroll. 13 | 14 | .border-bottom-radius( @select-input-border-radius ); 15 | background-color: @select-input-bg; 16 | border: 1px solid @select-input-border-color; 17 | border-top-color: mix(@select-input-bg, @select-input-border-color, 50%); 18 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 19 | box-sizing: border-box; 20 | margin-top: -1px; 21 | max-height: @select-menu-max-height; 22 | position: absolute; 23 | top: 100%; 24 | width: 100%; 25 | z-index: @select-menu-zindex; 26 | -webkit-overflow-scrolling: touch; 27 | } 28 | 29 | 30 | // wrapper 31 | 32 | .Select-menu { 33 | max-height: @select-menu-max-height - 2px; 34 | overflow-y: auto; 35 | } 36 | 37 | 38 | // options 39 | 40 | .Select-option { 41 | box-sizing: border-box; 42 | color: @select-option-color; 43 | cursor: pointer; 44 | display: block; 45 | padding: @select-padding-vertical @select-padding-horizontal; 46 | 47 | &:last-child { 48 | .border-bottom-radius( @select-input-border-radius ); 49 | } 50 | 51 | &.is-focused { 52 | background-color: @select-option-focused-bg; 53 | color: @select-option-focused-color; 54 | } 55 | 56 | &.is-disabled { 57 | color: @select-option-disabled-color; 58 | cursor: not-allowed; 59 | } 60 | 61 | } 62 | 63 | 64 | // no results 65 | 66 | .Select-noresults, 67 | .Select-search-prompt, 68 | .Select-searching { 69 | box-sizing: border-box; 70 | color: @select-noresults-color; 71 | cursor: default; 72 | display: block; 73 | padding: @select-padding-vertical @select-padding-horizontal; 74 | } 75 | -------------------------------------------------------------------------------- /src/Option.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classes = require('classnames'); 3 | 4 | var Option = React.createClass({ 5 | propTypes: { 6 | addLabelText: React.PropTypes.string, // string rendered in case of allowCreate option passed to ReactSelect 7 | className: React.PropTypes.string, // className (based on mouse position) 8 | mouseDown: React.PropTypes.func, // method to handle click on option element 9 | mouseEnter: React.PropTypes.func, // method to handle mouseEnter on option element 10 | mouseLeave: React.PropTypes.func, // method to handle mouseLeave on option element 11 | option: React.PropTypes.object.isRequired, // object that is base for that option 12 | renderFunc: React.PropTypes.func // method passed to ReactSelect component to render label text 13 | }, 14 | 15 | blockEvent: function(event) { 16 | event.preventDefault(); 17 | if ((event.target.tagName !== 'A') || !('href' in event.target)) { 18 | return; 19 | } 20 | 21 | if (event.target.target) { 22 | window.open(event.target.href); 23 | } else { 24 | window.location.href = event.target.href; 25 | } 26 | }, 27 | 28 | render: function() { 29 | var obj = this.props.option; 30 | var renderedLabel = this.props.renderFunc(obj); 31 | var optionClasses = classes(this.props.className, obj.className); 32 | 33 | return obj.disabled ? ( 34 |
37 | {renderedLabel} 38 |
39 | ) : ( 40 |
47 | { obj.create ? this.props.addLabelText.replace('{label}', obj.label) : renderedLabel } 48 |
49 | ); 50 | } 51 | }); 52 | 53 | module.exports = Option; 54 | -------------------------------------------------------------------------------- /lib/Option.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var classes = require('classnames'); 5 | 6 | var Option = React.createClass({ 7 | displayName: 'Option', 8 | 9 | propTypes: { 10 | addLabelText: React.PropTypes.string, // string rendered in case of allowCreate option passed to ReactSelect 11 | className: React.PropTypes.string, // className (based on mouse position) 12 | mouseDown: React.PropTypes.func, // method to handle click on option element 13 | mouseEnter: React.PropTypes.func, // method to handle mouseEnter on option element 14 | mouseLeave: React.PropTypes.func, // method to handle mouseLeave on option element 15 | option: React.PropTypes.object.isRequired, // object that is base for that option 16 | renderFunc: React.PropTypes.func // method passed to ReactSelect component to render label text 17 | }, 18 | 19 | blockEvent: function blockEvent(event) { 20 | event.preventDefault(); 21 | if (event.target.tagName !== 'A' || !('href' in event.target)) { 22 | return; 23 | } 24 | 25 | if (event.target.target) { 26 | window.open(event.target.href); 27 | } else { 28 | window.location.href = event.target.href; 29 | } 30 | }, 31 | 32 | render: function render() { 33 | var obj = this.props.option; 34 | var renderedLabel = this.props.renderFunc(obj); 35 | var optionClasses = classes(this.props.className, obj.className); 36 | 37 | return obj.disabled ? React.createElement( 38 | 'div', 39 | { className: optionClasses, 40 | onMouseDown: this.blockEvent, 41 | onClick: this.blockEvent }, 42 | renderedLabel 43 | ) : React.createElement( 44 | 'div', 45 | { className: optionClasses, 46 | style: obj.style, 47 | onMouseEnter: this.props.mouseEnter, 48 | onMouseLeave: this.props.mouseLeave, 49 | onMouseDown: this.props.mouseDown, 50 | onClick: this.props.mouseDown, 51 | title: obj.title }, 52 | obj.create ? this.props.addLabelText.replace('{label}', obj.label) : renderedLabel 53 | ); 54 | } 55 | }); 56 | 57 | module.exports = Option; -------------------------------------------------------------------------------- /less/select.less: -------------------------------------------------------------------------------- 1 | /** 2 | * React Select 3 | * ============ 4 | * Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/ 5 | * https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs 6 | * MIT License: https://github.com/keystonejs/react-select 7 | */ 8 | 9 | // Variables 10 | // ------------------------------ 11 | 12 | @select-input-bg: #fff; 13 | @select-text-color: #333; 14 | @select-input-border-color: #ccc; 15 | @select-input-border-radius: 4px; 16 | @select-input-placeholder: #aaa; 17 | @select-input-border-focus: #08c; // blue 18 | 19 | @select-padding-vertical: 8px; 20 | @select-padding-horizontal: 10px; 21 | 22 | @select-arrow-color: #999; 23 | @select-arrow-width: 5px; 24 | 25 | @select-menu-zindex: 1000; 26 | @select-menu-max-height: 200px; 27 | 28 | @select-option-color: lighten(@select-text-color, 20%); 29 | @select-option-focused-color: @select-text-color; 30 | @select-option-focused-bg: #f2f9fc; // pale blue 31 | @select-option-disabled-color: lighten(@select-text-color, 60%); 32 | 33 | @select-noresults-color: lighten(@select-text-color, 40%); 34 | 35 | @select-clear-color: #999; 36 | @select-clear-hover-color: #c0392b; // red 37 | 38 | @select-item-border-radius: 2px; 39 | @select-item-gutter: 2px; 40 | @select-item-padding-vertical: 3px; 41 | @select-item-padding-horizontal: 5px; 42 | @select-item-font-size: 1em; 43 | @select-item-color: #08c; // pale blue 44 | @select-item-bg: #f2f9fc; 45 | @select-item-border-color: darken(@select-item-bg, 10%); 46 | @select-item-hover-color: darken(@select-item-color, 5%); // pale blue 47 | @select-item-hover-bg: darken(@select-item-bg, 5%); 48 | @select-item-disabled-color: #888; 49 | @select-item-disabled-bg: #f2f2f2; 50 | @select-item-disabled-border-color: darken(@select-item-disabled-bg, 10%); 51 | 52 | 53 | @import "control.less"; 54 | @import "menu.less"; 55 | @import "mixins.less"; 56 | @import "multi.less"; 57 | @import "spinner.less"; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-select", 3 | "version": "0.6.11", 4 | "description": "A Select control built with and for ReactJS", 5 | "main": "lib/Select.js", 6 | "style": "dist/default.css", 7 | "author": "Jed Watson", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/JedWatson/react-select.git" 12 | }, 13 | "dependencies": { 14 | "classnames": "^2.1.3", 15 | "react-input-autosize": "^0.5.3" 16 | }, 17 | "devDependencies": { 18 | "babel": "^5.8.23", 19 | "babel-eslint": "^4.1.2", 20 | "chai": "^3.2.0", 21 | "coveralls": "^2.11.4", 22 | "eslint": "^1.4.3", 23 | "eslint-plugin-react": "^3.4.1", 24 | "gulp": "^3.9.0", 25 | "istanbul": "^0.3.20", 26 | "jsdom": "^3.1.2", 27 | "mocha": "^2.3.2", 28 | "react": ">=0.13.3 || ^0.14.0-beta", 29 | "react-gravatar": "^2.0.1", 30 | "react-component-gulp-tasks": "^0.7.1", 31 | "sinon": "^1.16.1", 32 | "unexpected": "^9.12.3", 33 | "unexpected-dom": "^1.3.0", 34 | "unexpected-sinon": "^7.1.1" 35 | }, 36 | "peerDependencies": { 37 | "react": ">=0.13.3 || ^0.14.0-rc1" 38 | }, 39 | "browserify-shim": { 40 | "classnames": "global:classNames", 41 | "react": "global:React", 42 | "react-input-autosize": "global:AutosizeInput" 43 | }, 44 | "scripts": { 45 | "build": "gulp clean && NODE_ENV=production gulp build", 46 | "bump": "gulp bump", 47 | "bump:major": "gulp bump:major", 48 | "bump:minor": "gulp bump:minor", 49 | "cover": "istanbul cover _mocha -- -u exports --compilers js:babel/register -R spec", 50 | "coveralls": "NODE_ENV=test istanbul cover _mocha --report lcovonly -- -u exports --compilers js:babel/register -R spec && cat coverage/lcov.info | coveralls", 51 | "examples": "gulp dev:server", 52 | "lint": "eslint .", 53 | "publish:examples": "NODE_ENV=production gulp publish:examples", 54 | "release": "NODE_ENV=production gulp release", 55 | "start": "gulp dev", 56 | "test": "mocha --compilers js:babel/register", 57 | "watch": "gulp watch:lib" 58 | }, 59 | "keywords": [ 60 | "combobox", 61 | "form", 62 | "input", 63 | "multiselect", 64 | "react", 65 | "react-component", 66 | "select", 67 | "ui" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /src/Value.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classes = require('classnames'); 3 | 4 | var Value = React.createClass({ 5 | 6 | displayName: 'Value', 7 | 8 | propTypes: { 9 | disabled: React.PropTypes.bool, // disabled prop passed to ReactSelect 10 | onOptionLabelClick: React.PropTypes.func, // method to handle click on value label 11 | onRemove: React.PropTypes.func, // method to handle remove of that value 12 | option: React.PropTypes.object.isRequired, // option passed to component 13 | optionLabelClick: React.PropTypes.bool, // indicates if onOptionLabelClick should be handled 14 | renderer: React.PropTypes.func // method to render option label passed to ReactSelect 15 | }, 16 | 17 | blockEvent: function(event) { 18 | event.stopPropagation(); 19 | }, 20 | 21 | handleOnRemove: function(event) { 22 | if (!this.props.disabled) { 23 | this.props.onRemove(event); 24 | } 25 | }, 26 | 27 | render: function() { 28 | var label = this.props.option.label; 29 | if (this.props.renderer) { 30 | label = this.props.renderer(this.props.option); 31 | } 32 | 33 | if(!this.props.onRemove && !this.props.optionLabelClick) { 34 | return ( 35 |
{label}
40 | ); 41 | } 42 | 43 | if (this.props.optionLabelClick) { 44 | 45 | label = ( 46 | 52 | {label} 53 | 54 | ); 55 | } 56 | 57 | return ( 58 |
61 | × 65 | {label} 66 |
67 | ); 68 | } 69 | 70 | }); 71 | 72 | module.exports = Value; 73 | -------------------------------------------------------------------------------- /examples/src/app.js: -------------------------------------------------------------------------------- 1 | /* eslint react/prop-types: 0 */ 2 | 3 | import React from 'react'; 4 | import Select from 'react-select'; 5 | 6 | import CustomRenderField from './components/CustomRenderField'; 7 | import MultiSelectField from './components/MultiSelectField'; 8 | import RemoteSelectField from './components/RemoteSelectField'; 9 | import SelectedValuesField from './components/SelectedValuesField'; 10 | import StatesField from './components/StatesField'; 11 | import UsersField from './components/UsersField'; 12 | import ValuesAsNumbersField from './components/ValuesAsNumbersField'; 13 | import DisabledUpsellOptions from './components/DisabledUpsellOptions'; 14 | 15 | var FLAVOURS = [ 16 | { label: 'Chocolate', value: 'chocolate' }, 17 | { label: 'Vanilla', value: 'vanilla' }, 18 | { label: 'Strawberry', value: 'strawberry' }, 19 | { label: 'Cookies and Cream', value: 'cookiescream' }, 20 | { label: 'Peppermint', value: 'peppermint' } 21 | ]; 22 | var FLAVOURS_WITH_DISABLED_OPTION = FLAVOURS.slice(0); 23 | FLAVOURS_WITH_DISABLED_OPTION.unshift({ label: 'Caramel (You don\'t like it, apparently)', value: 'caramel', disabled: true }); 24 | 25 | function logChange() { 26 | console.log.apply(console, [].concat(['Select value changed:'], Array.prototype.slice.apply(arguments))); 27 | } 28 | 29 | React.render( 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
, 44 | document.getElementById('example') 45 | ); 46 | -------------------------------------------------------------------------------- /lib/Value.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var classes = require('classnames'); 5 | 6 | var Value = React.createClass({ 7 | 8 | displayName: 'Value', 9 | 10 | propTypes: { 11 | disabled: React.PropTypes.bool, // disabled prop passed to ReactSelect 12 | onOptionLabelClick: React.PropTypes.func, // method to handle click on value label 13 | onRemove: React.PropTypes.func, // method to handle remove of that value 14 | option: React.PropTypes.object.isRequired, // option passed to component 15 | optionLabelClick: React.PropTypes.bool, // indicates if onOptionLabelClick should be handled 16 | renderer: React.PropTypes.func // method to render option label passed to ReactSelect 17 | }, 18 | 19 | blockEvent: function blockEvent(event) { 20 | event.stopPropagation(); 21 | }, 22 | 23 | handleOnRemove: function handleOnRemove(event) { 24 | if (!this.props.disabled) { 25 | this.props.onRemove(event); 26 | } 27 | }, 28 | 29 | render: function render() { 30 | var label = this.props.option.label; 31 | if (this.props.renderer) { 32 | label = this.props.renderer(this.props.option); 33 | } 34 | 35 | if (!this.props.onRemove && !this.props.optionLabelClick) { 36 | return React.createElement( 37 | 'div', 38 | { 39 | className: classes('Select-value', this.props.option.className), 40 | style: this.props.option.style, 41 | title: this.props.option.title 42 | }, 43 | label 44 | ); 45 | } 46 | 47 | if (this.props.optionLabelClick) { 48 | 49 | label = React.createElement( 50 | 'a', 51 | { className: classes('Select-item-label__a', this.props.option.className), 52 | onMouseDown: this.blockEvent, 53 | onTouchEnd: this.props.onOptionLabelClick, 54 | onClick: this.props.onOptionLabelClick, 55 | style: this.props.option.style, 56 | title: this.props.option.title }, 57 | label 58 | ); 59 | } 60 | 61 | return React.createElement( 62 | 'div', 63 | { className: classes('Select-item', this.props.option.className), 64 | style: this.props.option.style, 65 | title: this.props.option.title }, 66 | React.createElement( 67 | 'span', 68 | { className: 'Select-item-icon', 69 | onMouseDown: this.blockEvent, 70 | onClick: this.handleOnRemove, 71 | onTouchEnd: this.handleOnRemove }, 72 | '×' 73 | ), 74 | React.createElement( 75 | 'span', 76 | { className: 'Select-item-label' }, 77 | label 78 | ) 79 | ); 80 | } 81 | 82 | }); 83 | 84 | module.exports = Value; -------------------------------------------------------------------------------- /less/multi.less: -------------------------------------------------------------------------------- 1 | // 2 | // Multi-Select 3 | // ------------------------------ 4 | 5 | 6 | // Base 7 | 8 | .Select.is-multi { 9 | 10 | // control: reduce padding to allow for items 11 | .Select-control { 12 | padding: 13 | @select-padding-vertical - @select-item-gutter - @select-item-padding-vertical - 1 14 | @select-padding-horizontal * 3 + 22 15 | @select-padding-vertical - @select-item-gutter - @select-item-padding-vertical - 1 16 | @select-padding-horizontal - @select-item-gutter - @select-item-padding-horizontal 17 | } 18 | 19 | // add margin to the input element 20 | .Select-input { 21 | vertical-align: middle; 22 | border: 1px solid transparent; 23 | margin: @select-item-gutter; 24 | padding: @select-item-padding-vertical 0; 25 | } 26 | 27 | } 28 | 29 | // Items 30 | 31 | .Select-item { 32 | background-color: @select-item-bg; 33 | border-radius: @select-item-border-radius; 34 | border: 1px solid @select-item-border-color; 35 | color: @select-item-color; 36 | display: inline-block; 37 | font-size: @select-item-font-size; 38 | margin: @select-item-gutter; 39 | } 40 | 41 | // common 42 | .Select-item-icon, 43 | .Select-item-label { 44 | display: inline-block; 45 | vertical-align: middle; 46 | } 47 | 48 | // label 49 | .Select-item-label { 50 | cursor: default; 51 | .border-right-radius( @select-item-border-radius ); 52 | padding: @select-item-padding-vertical @select-item-padding-horizontal; 53 | 54 | .Select-item-label__a { 55 | color: @select-item-color; 56 | cursor: pointer; 57 | } 58 | } 59 | 60 | // icon 61 | .Select-item-icon { 62 | cursor: pointer; 63 | .border-left-radius( @select-item-border-radius ); 64 | border-right: 1px solid @select-item-border-color; 65 | 66 | // move the baseline up by 1px 67 | padding: @select-item-padding-vertical - 1 @select-item-padding-horizontal @select-item-padding-vertical + 1; 68 | 69 | &:hover, 70 | &:focus { 71 | background-color: @select-item-hover-bg; 72 | color: @select-item-hover-color; 73 | } 74 | &:active { 75 | background-color: @select-item-border-color; 76 | } 77 | } 78 | 79 | .Select.is-multi.is-disabled { 80 | .Select-item { 81 | background-color: @select-item-disabled-bg; 82 | border: 1px solid @select-item-disabled-border-color; 83 | color: @select-item-disabled-color; 84 | } 85 | // icon 86 | .Select-item-icon { 87 | cursor: not-allowed; 88 | border-right: 1px solid @select-item-disabled-border-color; 89 | 90 | &:hover, 91 | &:focus, 92 | &:active { 93 | background-color: @select-item-disabled-bg; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in React-Select. All forms of contribution are 4 | welcome, from issue reports to PRs and documentation / write-ups. 5 | 6 | Before you open a PR: 7 | 8 | * If you're planning to add or change a major feature in a PR, please ensure 9 | the change is aligned with the project roadmap by opening an issue first, 10 | especially if you're going to spend a lot of time on it. 11 | * In development, run `npm start` to build (+watch) the project source, and run 12 | the [development server](http://localhost:8000). 13 | * Please ensure all the examples work correctly after your change. If you're 14 | adding a major new use-case, add a new example demonstrating its use. 15 | * Please **do not** commit the build files. Make sure **only** your changes to 16 | `/src/`, `/less/` and `/examples/src` are included in your PR. 17 | * Be careful to follow the code style of the project. Run `npm run lint` after 18 | your changes and ensure you do not introduce any new errors or warnings. 19 | 20 | * Ensure that your effort is aligned with the project's roadmap by talking to 21 | the maintainers, especially if you are going to spend a lot of time on it. 22 | * Make sure there's an issue open for any work you take on and intend to submit 23 | as a pull request - it helps core members review your concept and direction 24 | early and is a good way to discuss what you're planning to do. 25 | * If you open an issue and are interested in working on a fix, please let us 26 | know. We'll help you get started, rather than adding it to the queue. 27 | * Make sure you do not add regressions by running `npm test`. 28 | * Where possible, include tests with your changes, either that demonstrates the 29 | bug, or tests the new functionality. If you're not sure how to test your 30 | changes, feel free to ping @bruderstein 31 | * Run `npm run cover` to check that the coverage hasn't dropped, and look at the 32 | report (under the generated `coverage` directory) to check that your changes are 33 | covered 34 | * Please [follow our established coding conventions](https://github.com/keystonejs/keystone/wiki/Coding-Standards) 35 | (with regards to formatting, etc) 36 | * You can also run `npm run lint` and `npm run style` - our linter is a WIP 37 | but please ensure there are not more violations than before your changes. 38 | * All new features and changes need documentation. We have three translations, 39 | please read our [Documentation Guidelines](https://github.com/keystonejs/keystone/wiki/Documentation-Translation-Guidelines). 40 | 41 | * _Make sure you revert your build before submitting a PR_ to reduce the change 42 | of conflicts. `gulp build-scripts` is run after PRs are merged and before any 43 | releases are made. 44 | -------------------------------------------------------------------------------- /test/Value-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global describe, it, beforeEach */ 3 | 4 | var helper = require('../testHelpers/jsdomHelper'); 5 | helper(); 6 | 7 | var unexpected = require('unexpected'); 8 | var unexpectedDom = require('unexpected-dom'); 9 | var unexpectedSinon = require('unexpected-sinon'); 10 | var sinon = require('sinon'); 11 | 12 | var expect = unexpected 13 | .clone() 14 | .installPlugin(unexpectedSinon) 15 | .installPlugin(unexpectedDom); 16 | 17 | var React = require('react/addons'); 18 | var TestUtils = React.addons.TestUtils; 19 | 20 | var OPTION = { label: 'TEST-LABEL', value: 'TEST-VALUE' }; 21 | 22 | var Value = require('../src/Value'); 23 | 24 | describe('Value component', function() { 25 | 26 | var props; 27 | var value; 28 | 29 | beforeEach(function() { 30 | props = { 31 | option: OPTION, 32 | onRemove: sinon.spy() 33 | }; 34 | value = TestUtils.renderIntoDocument(); 35 | }); 36 | 37 | it('requests its own removal when the remove icon is clicked', function() { 38 | var selectItemIcon = TestUtils.findRenderedDOMComponentWithClass(value, 'Select-item-icon'); 39 | TestUtils.Simulate.click(selectItemIcon); 40 | expect(props.onRemove, 'was called'); 41 | }); 42 | 43 | it('requests its own removal when the remove icon is touched', function() { 44 | var selectItemIcon = TestUtils.findRenderedDOMComponentWithClass(value, 'Select-item-icon'); 45 | TestUtils.Simulate.touchEnd(selectItemIcon); 46 | expect(props.onRemove, 'was called'); 47 | }); 48 | 49 | it('prevents event propagation, pt 1', function() { 50 | var mockEvent = { stopPropagation: sinon.spy() }; 51 | value.blockEvent(mockEvent); 52 | expect(mockEvent.stopPropagation, 'was called'); 53 | }); 54 | 55 | describe('without a custom click handler', function() { 56 | 57 | it('presents the given label', function() { 58 | var selectItemLabel = TestUtils.findRenderedDOMComponentWithClass(value, 'Select-item-label'); 59 | expect(React.findDOMNode(selectItemLabel), 'to have text', OPTION.label); 60 | }); 61 | 62 | }); 63 | 64 | describe('with a custom click handler', function() { 65 | var selectItemLabelA; 66 | 67 | beforeEach(function() { 68 | props = { 69 | option: OPTION, 70 | onRemove: sinon.spy(), 71 | optionLabelClick: true, 72 | onOptionLabelClick: sinon.spy() 73 | }; 74 | value = TestUtils.renderIntoDocument(); 75 | selectItemLabelA = TestUtils.findRenderedDOMComponentWithClass(value, 'Select-item-label__a'); 76 | }); 77 | 78 | it('presents the given label', function() { 79 | expect(React.findDOMNode(selectItemLabelA), 'to have text', OPTION.label); 80 | }); 81 | 82 | it('calls a custom callback when the anchor is clicked', function() { 83 | TestUtils.Simulate.click(selectItemLabelA); 84 | expect(props.onOptionLabelClick, 'was called'); 85 | }); 86 | 87 | it('calls a custom callback when the anchor is touched', function() { 88 | TestUtils.Simulate.touchEnd(selectItemLabelA); 89 | expect(props.onOptionLabelClick, 'was called'); 90 | }); 91 | 92 | }); 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /examples/src/components/StatesField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from 'react-select'; 3 | 4 | const STATES = require('../data/states'); 5 | var id = 0; 6 | 7 | function logChange() { 8 | console.log.apply(console, [].concat(['Select value changed:'], Array.prototype.slice.apply(arguments))); 9 | } 10 | 11 | var StatesField = React.createClass({ 12 | displayName: 'StatesField', 13 | propTypes: { 14 | label: React.PropTypes.string, 15 | searchable: React.PropTypes.bool, 16 | }, 17 | getDefaultProps () { 18 | return { 19 | label: 'States:', 20 | searchable: true, 21 | }; 22 | }, 23 | getInitialState () { 24 | return { 25 | country: 'AU', 26 | disabled: false, 27 | searchable: this.props.searchable, 28 | id: ++id, 29 | selectValue: 'new-south-wales' 30 | }; 31 | }, 32 | switchCountry (e) { 33 | var newCountry = e.target.value; 34 | console.log('Country changed to ' + newCountry); 35 | this.setState({ 36 | country: newCountry, 37 | selectValue: null 38 | }); 39 | }, 40 | updateValue (newValue) { 41 | logChange('State changed to ' + newValue); 42 | this.setState({ 43 | selectValue: newValue || null 44 | }); 45 | }, 46 | focusStateSelect () { 47 | this.refs.stateSelect.focus(); 48 | }, 49 | toggleCheckbox (e) { 50 | let newState = {}; 51 | newState[e.target.name] = e.target.checked; 52 | this.setState(newState); 53 | }, 54 | render () { 55 | var ops = STATES[this.state.country]; 56 | return ( 57 |
58 |

{this.props.label}

59 | 65 | Searchable 66 | 67 | 71 |
72 |
73 | 77 | 81 |
82 | 83 | ); 84 | } 85 | }); 86 | 87 | 88 | module.exports = StatesField; 89 | -------------------------------------------------------------------------------- /examples/src/data/states.js: -------------------------------------------------------------------------------- 1 | exports.AU = [ 2 | { value: 'australian-capital-territory', label: 'Australian Capital Territory' }, 3 | { value: 'new-south-wales', label: 'New South Wales' }, 4 | { value: 'victoria', label: 'Victoria' }, 5 | { value: 'queensland', label: 'Queensland' }, 6 | { value: 'western-australia', label: 'Western Australia' }, 7 | { value: 'south-australia', label: 'South Australia' }, 8 | { value: 'tasmania', label: 'Tasmania' }, 9 | { value: 'northern-territory', label: 'Northern Territory' } 10 | ]; 11 | 12 | exports.US = [ 13 | { value: 'AL', label: 'Alabama', disabled: true }, 14 | { value: 'AK', label: 'Alaska' }, 15 | { value: 'AS', label: 'American Samoa' }, 16 | { value: 'AZ', label: 'Arizona' }, 17 | { value: 'AR', label: 'Arkansas' }, 18 | { value: 'CA', label: 'California' }, 19 | { value: 'CO', label: 'Colorado' }, 20 | { value: 'CT', label: 'Connecticut' }, 21 | { value: 'DE', label: 'Delaware' }, 22 | { value: 'DC', label: 'District Of Columbia' }, 23 | { value: 'FM', label: 'Federated States Of Micronesia' }, 24 | { value: 'FL', label: 'Florida' }, 25 | { value: 'GA', label: 'Georgia' }, 26 | { value: 'GU', label: 'Guam' }, 27 | { value: 'HI', label: 'Hawaii' }, 28 | { value: 'ID', label: 'Idaho' }, 29 | { value: 'IL', label: 'Illinois' }, 30 | { value: 'IN', label: 'Indiana' }, 31 | { value: 'IA', label: 'Iowa' }, 32 | { value: 'KS', label: 'Kansas' }, 33 | { value: 'KY', label: 'Kentucky' }, 34 | { value: 'LA', label: 'Louisiana' }, 35 | { value: 'ME', label: 'Maine' }, 36 | { value: 'MH', label: 'Marshall Islands' }, 37 | { value: 'MD', label: 'Maryland' }, 38 | { value: 'MA', label: 'Massachusetts' }, 39 | { value: 'MI', label: 'Michigan' }, 40 | { value: 'MN', label: 'Minnesota' }, 41 | { value: 'MS', label: 'Mississippi' }, 42 | { value: 'MO', label: 'Missouri' }, 43 | { value: 'MT', label: 'Montana' }, 44 | { value: 'NE', label: 'Nebraska' }, 45 | { value: 'NV', label: 'Nevada' }, 46 | { value: 'NH', label: 'New Hampshire' }, 47 | { value: 'NJ', label: 'New Jersey' }, 48 | { value: 'NM', label: 'New Mexico' }, 49 | { value: 'NY', label: 'New York' }, 50 | { value: 'NC', label: 'North Carolina' }, 51 | { value: 'ND', label: 'North Dakota' }, 52 | { value: 'MP', label: 'Northern Mariana Islands' }, 53 | { value: 'OH', label: 'Ohio' }, 54 | { value: 'OK', label: 'Oklahoma' }, 55 | { value: 'OR', label: 'Oregon' }, 56 | { value: 'PW', label: 'Palau' }, 57 | { value: 'PA', label: 'Pennsylvania' }, 58 | { value: 'PR', label: 'Puerto Rico' }, 59 | { value: 'RI', label: 'Rhode Island' }, 60 | { value: 'SC', label: 'South Carolina' }, 61 | { value: 'SD', label: 'South Dakota' }, 62 | { value: 'TN', label: 'Tennessee' }, 63 | { value: 'TX', label: 'Texas' }, 64 | { value: 'UT', label: 'Utah' }, 65 | { value: 'VT', label: 'Vermont' }, 66 | { value: 'VI', label: 'Virgin Islands' }, 67 | { value: 'VA', label: 'Virginia' }, 68 | { value: 'WA', label: 'Washington' }, 69 | { value: 'WV', label: 'West Virginia' }, 70 | { value: 'WI', label: 'Wisconsin' }, 71 | { value: 'WY', label: 'Wyoming' } 72 | ]; 73 | -------------------------------------------------------------------------------- /examples/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React-Select Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 |
25 |
26 | Code and Docs on GitHub 27 | 28 | Star 29 | 30 |
31 |
32 |
33 |
34 |
35 | 36 | 40 | 41 |
42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 | Copyright © Jed Watson 2015. MIT Licensed. 51 |
52 |
53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/src/components/ValuesAsNumbersField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from 'react-select'; 3 | 4 | function logChange() { 5 | console.log.apply(console, [].concat(['Select value changed:'], Array.prototype.slice.apply(arguments))); 6 | } 7 | 8 | var ValuesAsNumbersField = React.createClass({ 9 | displayName: 'ValuesAsNumbersField', 10 | propTypes: { 11 | label: React.PropTypes.string 12 | }, 13 | 14 | getInitialState () { 15 | return { 16 | options: [ 17 | { value: 10, label: 'Ten' }, 18 | { value: 11, label: 'Eleven' }, 19 | { value: 12, label: 'Twelve' }, 20 | { value: 23, label: 'Twenty-three' }, 21 | { value: 24, label: 'Twenty-four' } 22 | ], 23 | matchPos: 'any', 24 | matchValue: true, 25 | matchLabel: true, 26 | value: null, 27 | multi: false 28 | }; 29 | }, 30 | 31 | onChangeMatchStart(event) { 32 | this.setState({ 33 | matchPos: event.target.checked ? 'start' : 'any' 34 | }); 35 | }, 36 | 37 | onChangeMatchValue(event) { 38 | this.setState({ 39 | matchValue: event.target.checked 40 | }); 41 | }, 42 | 43 | onChangeMatchLabel(event) { 44 | this.setState({ 45 | matchLabel: event.target.checked 46 | }); 47 | }, 48 | 49 | onChange(value, values) { 50 | this.setState({ 51 | value: value 52 | }); 53 | logChange(value, values); 54 | }, 55 | 56 | onChangeMulti(event) { 57 | this.setState({ 58 | multi: event.target.checked 59 | }); 60 | }, 61 | 62 | render () { 63 | 64 | var matchProp = 'any'; 65 | 66 | if (this.state.matchLabel && !this.state.matchValue) { 67 | matchProp = 'label'; 68 | } 69 | 70 | if (!this.state.matchLabel && this.state.matchValue) { 71 | matchProp = 'value'; 72 | } 73 | 74 | return ( 75 |
76 |

{this.props.label}

77 | 89 | Multi-Select 90 | 91 | 95 | 99 | 103 |
104 | 105 | ); 106 | } 107 | }); 108 | 109 | module.exports = ValuesAsNumbersField; -------------------------------------------------------------------------------- /examples/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React-Select Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 |
25 |
26 | Code and Docs on GitHub 27 | 28 | Star 29 | 30 |
31 |
32 |
33 |
34 |
35 | 36 | 40 | 41 |
42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 | Copyright © Jed Watson 2015. MIT Licensed. 51 |
52 |
53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /less/control.less: -------------------------------------------------------------------------------- 1 | // 2 | // Control 3 | // ------------------------------ 4 | 5 | .Select { 6 | position: relative; 7 | } 8 | 9 | // base 10 | 11 | .Select-control { 12 | position: relative; 13 | overflow: hidden; 14 | background-color: @select-input-bg; 15 | border: 1px solid @select-input-border-color; 16 | border-color: lighten(@select-input-border-color, 5%) @select-input-border-color darken(@select-input-border-color, 10%); 17 | border-radius: @select-input-border-radius; 18 | box-sizing: border-box; 19 | color: @select-text-color; 20 | cursor: default; 21 | outline: none; 22 | // right padding allows for arrow, clear button and loading indicator 23 | padding: @select-padding-vertical 52px @select-padding-vertical @select-padding-horizontal; 24 | // transition: all 200ms ease; // TODO: find out why this introduced. removed to fix #401 25 | 26 | &:hover { 27 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 28 | } 29 | } 30 | 31 | .is-searchable { 32 | &.is-open > .Select-control { 33 | cursor: text; 34 | } 35 | } 36 | 37 | .is-open > .Select-control { 38 | .border-bottom-radius( 0 ); 39 | background: @select-input-bg; 40 | border-color: darken(@select-input-border-color, 10%) @select-input-border-color lighten(@select-input-border-color, 5%); 41 | 42 | // flip the arrow so its pointing up when the menu is open 43 | > .Select-arrow { 44 | border-color: transparent transparent @select-arrow-color; 45 | border-width: 0 @select-arrow-width @select-arrow-width; 46 | } 47 | } 48 | 49 | .is-searchable { 50 | &.is-focused:not(.is-open) > .Select-control { 51 | cursor: text; 52 | } 53 | } 54 | 55 | .is-focused:not(.is-open) > .Select-control { 56 | border-color: @select-input-border-focus lighten(@select-input-border-focus, 5%) lighten(@select-input-border-focus, 5%); 57 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px fade(@select-input-border-focus,50%); 58 | } 59 | 60 | // placeholder 61 | 62 | .Select-placeholder { 63 | color: @select-input-placeholder; 64 | padding: @select-padding-vertical 52px @select-padding-vertical @select-padding-horizontal; 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | right: -(@select-arrow-width + @select-padding-horizontal); 69 | 70 | // crop text 71 | max-width: 100%; 72 | overflow: hidden; 73 | text-overflow: ellipsis; 74 | white-space: nowrap; 75 | } 76 | 77 | .has-value > .Select-control > .Select-placeholder { 78 | color: @select-text-color; 79 | } 80 | 81 | // value with custom renderer 82 | 83 | .Select-value { 84 | color: @select-input-placeholder; 85 | padding: @select-padding-vertical 52px @select-padding-vertical @select-padding-horizontal; 86 | position: absolute; 87 | top: 0; 88 | left: 0; 89 | right: -(@select-arrow-width + @select-padding-horizontal); 90 | 91 | // crop text 92 | max-width: 100%; 93 | overflow: hidden; 94 | text-overflow: ellipsis; 95 | white-space: nowrap; 96 | } 97 | 98 | .has-value > .Select-control > .Select-value { 99 | color: @select-text-color; 100 | } 101 | 102 | 103 | // the element users type in 104 | 105 | .Select-input { 106 | 107 | > input { 108 | cursor: default; 109 | background: none transparent; 110 | box-shadow: none; 111 | height: auto; 112 | border: 0 none; 113 | font-family: inherit; 114 | font-size: inherit; 115 | margin: 0; 116 | padding: 0; 117 | outline: none; 118 | display: inline-block; 119 | -webkit-appearance: none; 120 | 121 | .is-focused & { 122 | cursor: text; 123 | } 124 | } 125 | 126 | } 127 | 128 | // fake input 129 | .Select-control:not(.is-searchable) > .Select-input { 130 | outline: none; 131 | } 132 | 133 | 134 | // loading indicator 135 | 136 | .Select-loading { 137 | .Select-spinner(16px, @select-input-border-color, @select-text-color); 138 | margin-top: -8px; 139 | position: absolute; 140 | right: (@select-padding-horizontal * 3); 141 | top: 50%; 142 | } 143 | 144 | .has-value > .Select-control > .Select-loading { 145 | right: (@select-padding-horizontal * 3) + 16px; 146 | } 147 | 148 | 149 | // the little cross that clears the field 150 | 151 | .Select-clear { 152 | // todo: animate transition in 153 | color: @select-clear-color; 154 | cursor: pointer; 155 | display: inline-block; 156 | font-size: 16px; 157 | padding: @select-padding-vertical - 2 @select-padding-horizontal; 158 | position: absolute; 159 | right: @select-padding-horizontal + @select-arrow-width + 2; 160 | top: 0; 161 | 162 | &:hover { 163 | color: @select-clear-hover-color; 164 | } 165 | 166 | > span { 167 | font-size: 1.1em; 168 | } 169 | } 170 | 171 | 172 | // arrow indicator 173 | 174 | .Select-arrow-zone { 175 | content: " "; 176 | display: block; 177 | position: absolute; 178 | right: 0; 179 | top: 0; 180 | bottom: 0; 181 | width: 30px; 182 | cursor: pointer; 183 | } 184 | 185 | .Select-arrow { 186 | border-color: @select-arrow-color transparent transparent; 187 | border-style: solid; 188 | border-width: @select-arrow-width @select-arrow-width 0; 189 | content: " "; 190 | display: block; 191 | height: 0; 192 | margin-top: -ceil(@select-arrow-width / 2); 193 | position: absolute; 194 | right: @select-padding-horizontal; 195 | top: @select-padding-vertical + @select-arrow-width + 1; 196 | width: 0; 197 | cursor: pointer; 198 | } 199 | -------------------------------------------------------------------------------- /examples/src/example.less: -------------------------------------------------------------------------------- 1 | // 2 | // Common Example Styles 3 | // ------------------------------ 4 | 5 | 6 | 7 | 8 | // import select field styles 9 | 10 | @import "../../less/select.less"; 11 | 12 | 13 | 14 | 15 | // Constants 16 | // ------------------------------ 17 | 18 | // example site 19 | 20 | @heading-color: black; 21 | @text-color: #333; 22 | @link-color: #007eff; 23 | @gutter: 30px; 24 | 25 | // diverge from react-select defaults 26 | 27 | @select-item-bg: fade(@link-color, 8%); 28 | @select-item-border-color: fade(@link-color, 24%); 29 | @select-item-color: @link-color; 30 | 31 | 32 | 33 | 34 | // Base 35 | // ------------------------------ 36 | 37 | body { 38 | color: @text-color; 39 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 40 | font-size: 14px; 41 | line-height: 1.4; 42 | margin: 0; 43 | padding: 0; 44 | } 45 | 46 | a { 47 | color: @link-color; 48 | text-decoration: none; 49 | 50 | &:hover { 51 | text-decoration: underline; 52 | } 53 | } 54 | 55 | .container { 56 | margin-left: auto; 57 | margin-right: auto; 58 | max-width: 400px; 59 | padding: 0 @gutter; 60 | } 61 | 62 | 63 | 64 | 65 | // Headings 66 | // ------------------------------ 67 | 68 | h1, h2, h3, h4, h5, h6, 69 | .h1, .h2, .h3, .h4, .h5, .h6 { 70 | color: @heading-color; 71 | font-weight: 500; 72 | line-height: 1; 73 | margin-bottom: .66em; 74 | margin-top: 0; 75 | } 76 | 77 | h1, .h1 { 78 | font-size: 3em; 79 | } 80 | h2, .h2 { 81 | font-size: 2em; 82 | font-weight: 300; 83 | } 84 | h3, .h3 { 85 | font-size: 1.25em; 86 | // text-transform: uppercase; 87 | } 88 | h4, .h4 { 89 | font-size: 1em; 90 | } 91 | h5, .h5 { 92 | font-size: .85em; 93 | } 94 | h6, .h6 { 95 | font-size: .75em; 96 | } 97 | 98 | 99 | 100 | 101 | // Layout 102 | // ------------------------------ 103 | 104 | // common 105 | 106 | .page-body, 107 | .page-footer, 108 | .page-header { 109 | padding: @gutter 0; 110 | } 111 | 112 | // header 113 | 114 | .page-header { 115 | background-color: @link-color; 116 | color: mix(white, @link-color, 75%); 117 | 118 | h1, h2, h3 { 119 | color: white; 120 | } 121 | p { 122 | font-size: 1.2em; 123 | margin: 0; 124 | } 125 | a { 126 | border-bottom: 1px solid fade(white, 30%); 127 | color: white; 128 | text-decoration: none; 129 | 130 | &:hover, 131 | &:focus { 132 | border-bottom-color: white; 133 | outline: none; 134 | text-decoration: none; 135 | } 136 | } 137 | } 138 | 139 | // subheader 140 | 141 | .page-subheader { 142 | background-color: mix(@link-color, white, 10%); 143 | line-height: 20px; 144 | padding: @gutter 0; 145 | } 146 | .page-subheader__button { 147 | float: right; 148 | } 149 | .page-subheader__link { 150 | border-bottom: 1px solid fade(@link-color, 30%); 151 | outline: none; 152 | text-decoration: none; 153 | 154 | &:hover, 155 | &:focus { 156 | border-bottom-color: @link-color; 157 | outline: none; 158 | text-decoration: none; 159 | } 160 | } 161 | 162 | // footer 163 | 164 | .page-footer { 165 | background-color: #fafafa; 166 | // border-top: 1px solid #eee; 167 | color: #999; 168 | padding: @gutter 0; 169 | text-align: center; 170 | } 171 | 172 | // layout changes based on screen dimensions 173 | 174 | @media (min-width: 480px) { 175 | .page-body, 176 | .page-header { 177 | padding: (@gutter * 2) 0; 178 | } 179 | .page-header { 180 | font-size: 1.4em; 181 | } 182 | .page-subheader { 183 | font-size: 1.125em; 184 | line-height: 28px; 185 | } 186 | } 187 | 188 | 189 | 190 | 191 | // Form Styles 192 | // ------------------------------ 193 | 194 | .checkbox-list { 195 | margin-top: .5em; 196 | overflow: hidden; 197 | } 198 | .checkbox-list > .checkbox { 199 | clear: left; 200 | float: left; 201 | margin-top: .5em; 202 | } 203 | .checkbox-control { 204 | margin-right: .5em; 205 | position: relative; 206 | top: -1px; 207 | } 208 | .checkbox-label {} 209 | 210 | 211 | 212 | 213 | // Switcher 214 | // ------------------------------ 215 | 216 | .switcher { 217 | color: #999; 218 | cursor: default; 219 | font-size: 12px; 220 | margin: 10px 0; 221 | text-transform: uppercase; 222 | 223 | .link { 224 | color: @link-color; 225 | cursor: pointer; 226 | font-weight: bold; 227 | margin-left: 10px; 228 | 229 | &:hover { 230 | text-decoration: underline; 231 | } 232 | } 233 | .active { 234 | color: #666; 235 | font-weight: bold; 236 | margin-left: 10px; 237 | } 238 | } 239 | 240 | 241 | 242 | 243 | // Miscellaneous 244 | // ------------------------------ 245 | 246 | .section { 247 | margin-bottom: 40px; 248 | } 249 | 250 | .hint { 251 | font-size: .85em; 252 | margin: 15px 0; 253 | color: #666; 254 | } 255 | 256 | /* 257 | 258 | // include these styles to test normal form fields 259 | 260 | .form-input { 261 | margin-bottom: 15px; 262 | } 263 | 264 | .form-input input { 265 | background-color: white; 266 | border-color: lighten(@select-input-border-color, 5%) @select-input-border-color darken(@select-input-border-color, 10%); 267 | border-radius: @select-input-border-radius; 268 | border: 1px solid @select-input-border-color; 269 | box-sizing: border-box; 270 | color: @select-text-color; 271 | font-size: 14px; 272 | outline: none; 273 | padding: @select-padding-vertical @select-padding-horizontal; 274 | transition: all 200ms ease; 275 | width: 100%; 276 | 277 | &:hover { 278 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 279 | } 280 | 281 | &:focus { 282 | border-color: @select-input-border-focus lighten(@select-input-border-focus, 5%) lighten(@select-input-border-focus, 5%); 283 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px fade(@select-input-border-focus,50%); 284 | } 285 | } 286 | 287 | */ 288 | -------------------------------------------------------------------------------- /dist/default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * React Select 3 | * ============ 4 | * Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/ 5 | * https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs 6 | * MIT License: https://github.com/keystonejs/react-select 7 | */ 8 | .Select { 9 | position: relative; 10 | } 11 | .Select-control { 12 | position: relative; 13 | overflow: hidden; 14 | background-color: #ffffff; 15 | border: 1px solid #cccccc; 16 | border-color: #d9d9d9 #cccccc #b3b3b3; 17 | border-radius: 4px; 18 | box-sizing: border-box; 19 | color: #333333; 20 | cursor: default; 21 | outline: none; 22 | padding: 8px 52px 8px 10px; 23 | } 24 | .Select-control:hover { 25 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 26 | } 27 | .is-searchable.is-open > .Select-control { 28 | cursor: text; 29 | } 30 | .is-open > .Select-control { 31 | border-bottom-right-radius: 0; 32 | border-bottom-left-radius: 0; 33 | background: #ffffff; 34 | border-color: #b3b3b3 #cccccc #d9d9d9; 35 | } 36 | .is-open > .Select-control > .Select-arrow { 37 | border-color: transparent transparent #999999; 38 | border-width: 0 5px 5px; 39 | } 40 | .is-searchable.is-focused:not(.is-open) > .Select-control { 41 | cursor: text; 42 | } 43 | .is-focused:not(.is-open) > .Select-control { 44 | border-color: #0088cc #0099e6 #0099e6; 45 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px rgba(0, 136, 204, 0.5); 46 | } 47 | .Select-placeholder { 48 | color: #aaaaaa; 49 | padding: 8px 52px 8px 10px; 50 | position: absolute; 51 | top: 0; 52 | left: 0; 53 | right: -15px; 54 | max-width: 100%; 55 | overflow: hidden; 56 | text-overflow: ellipsis; 57 | white-space: nowrap; 58 | } 59 | .has-value > .Select-control > .Select-placeholder { 60 | color: #333333; 61 | } 62 | .Select-value { 63 | color: #aaaaaa; 64 | padding: 8px 52px 8px 10px; 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | right: -15px; 69 | max-width: 100%; 70 | overflow: hidden; 71 | text-overflow: ellipsis; 72 | white-space: nowrap; 73 | } 74 | .has-value > .Select-control > .Select-value { 75 | color: #333333; 76 | } 77 | .Select-input > input { 78 | cursor: default; 79 | background: none transparent; 80 | box-shadow: none; 81 | height: auto; 82 | border: 0 none; 83 | font-family: inherit; 84 | font-size: inherit; 85 | margin: 0; 86 | padding: 0; 87 | outline: none; 88 | display: inline-block; 89 | -webkit-appearance: none; 90 | } 91 | .is-focused .Select-input > input { 92 | cursor: text; 93 | } 94 | .Select-control:not(.is-searchable) > .Select-input { 95 | outline: none; 96 | } 97 | .Select-loading { 98 | -webkit-animation: Select-animation-spin 400ms infinite linear; 99 | -o-animation: Select-animation-spin 400ms infinite linear; 100 | animation: Select-animation-spin 400ms infinite linear; 101 | width: 16px; 102 | height: 16px; 103 | box-sizing: border-box; 104 | border-radius: 50%; 105 | border: 2px solid #cccccc; 106 | border-right-color: #333333; 107 | display: inline-block; 108 | position: relative; 109 | margin-top: -8px; 110 | position: absolute; 111 | right: 30px; 112 | top: 50%; 113 | } 114 | .has-value > .Select-control > .Select-loading { 115 | right: 46px; 116 | } 117 | .Select-clear { 118 | color: #999999; 119 | cursor: pointer; 120 | display: inline-block; 121 | font-size: 16px; 122 | padding: 6px 10px; 123 | position: absolute; 124 | right: 17px; 125 | top: 0; 126 | } 127 | .Select-clear:hover { 128 | color: #c0392b; 129 | } 130 | .Select-clear > span { 131 | font-size: 1.1em; 132 | } 133 | .Select-arrow-zone { 134 | content: " "; 135 | display: block; 136 | position: absolute; 137 | right: 0; 138 | top: 0; 139 | bottom: 0; 140 | width: 30px; 141 | cursor: pointer; 142 | } 143 | .Select-arrow { 144 | border-color: #999999 transparent transparent; 145 | border-style: solid; 146 | border-width: 5px 5px 0; 147 | content: " "; 148 | display: block; 149 | height: 0; 150 | margin-top: -ceil(2.5px); 151 | position: absolute; 152 | right: 10px; 153 | top: 14px; 154 | width: 0; 155 | cursor: pointer; 156 | } 157 | .Select-menu-outer { 158 | border-bottom-right-radius: 4px; 159 | border-bottom-left-radius: 4px; 160 | background-color: #ffffff; 161 | border: 1px solid #cccccc; 162 | border-top-color: #e6e6e6; 163 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 164 | box-sizing: border-box; 165 | margin-top: -1px; 166 | max-height: 200px; 167 | position: absolute; 168 | top: 100%; 169 | width: 100%; 170 | z-index: 1000; 171 | -webkit-overflow-scrolling: touch; 172 | } 173 | .Select-menu { 174 | max-height: 198px; 175 | overflow-y: auto; 176 | } 177 | .Select-option { 178 | box-sizing: border-box; 179 | color: #666666; 180 | cursor: pointer; 181 | display: block; 182 | padding: 8px 10px; 183 | } 184 | .Select-option:last-child { 185 | border-bottom-right-radius: 4px; 186 | border-bottom-left-radius: 4px; 187 | } 188 | .Select-option.is-focused { 189 | background-color: #f2f9fc; 190 | color: #333333; 191 | } 192 | .Select-option.is-disabled { 193 | color: #cccccc; 194 | cursor: not-allowed; 195 | } 196 | .Select-noresults, 197 | .Select-search-prompt, 198 | .Select-searching { 199 | box-sizing: border-box; 200 | color: #999999; 201 | cursor: default; 202 | display: block; 203 | padding: 8px 10px; 204 | } 205 | .Select.is-multi .Select-control { 206 | padding: 2px 52px 2px 3px; 207 | } 208 | .Select.is-multi .Select-input { 209 | vertical-align: middle; 210 | border: 1px solid transparent; 211 | margin: 2px; 212 | padding: 3px 0; 213 | } 214 | .Select-item { 215 | background-color: #f2f9fc; 216 | border-radius: 2px; 217 | border: 1px solid #c9e6f2; 218 | color: #0088cc; 219 | display: inline-block; 220 | font-size: 1em; 221 | margin: 2px; 222 | } 223 | .Select-item-icon, 224 | .Select-item-label { 225 | display: inline-block; 226 | vertical-align: middle; 227 | } 228 | .Select-item-label { 229 | cursor: default; 230 | border-bottom-right-radius: 2px; 231 | border-top-right-radius: 2px; 232 | padding: 3px 5px; 233 | } 234 | .Select-item-label .Select-item-label__a { 235 | color: #0088cc; 236 | cursor: pointer; 237 | } 238 | .Select-item-icon { 239 | cursor: pointer; 240 | border-bottom-left-radius: 2px; 241 | border-top-left-radius: 2px; 242 | border-right: 1px solid #c9e6f2; 243 | padding: 2px 5px 4px; 244 | } 245 | .Select-item-icon:hover, 246 | .Select-item-icon:focus { 247 | background-color: #ddeff7; 248 | color: #0077b3; 249 | } 250 | .Select-item-icon:active { 251 | background-color: #c9e6f2; 252 | } 253 | .Select.is-multi.is-disabled .Select-item { 254 | background-color: #f2f2f2; 255 | border: 1px solid #d9d9d9; 256 | color: #888888; 257 | } 258 | .Select.is-multi.is-disabled .Select-item-icon { 259 | cursor: not-allowed; 260 | border-right: 1px solid #d9d9d9; 261 | } 262 | .Select.is-multi.is-disabled .Select-item-icon:hover, 263 | .Select.is-multi.is-disabled .Select-item-icon:focus, 264 | .Select.is-multi.is-disabled .Select-item-icon:active { 265 | background-color: #f2f2f2; 266 | } 267 | @keyframes Select-animation-spin { 268 | to { 269 | transform: rotate(1turn); 270 | } 271 | } 272 | @-webkit-keyframes Select-animation-spin { 273 | to { 274 | -webkit-transform: rotate(1turn); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/JedWatson/react-select.svg?branch=master)](https://travis-ci.org/JedWatson/react-select) 2 | [![Coverage Status](https://coveralls.io/repos/JedWatson/react-select/badge.svg?branch=master&service=github)](https://coveralls.io/github/JedWatson/react-select?branch=master) 3 | 4 | React-Select 5 | ============ 6 | 7 | A Select control built with and for [React](http://facebook.github.io/react/index.html). Initially built for use in [KeystoneJS](http://www.keystonejs.com). 8 | 9 | 10 | ## Demo & Examples 11 | 12 | Live demo: [jedwatson.github.io/react-select](http://jedwatson.github.io/react-select/) 13 | 14 | To build the examples locally, run: 15 | 16 | ``` 17 | npm install 18 | npm start 19 | ``` 20 | 21 | Then open [`localhost:8000`](http://localhost:8000) in a browser. 22 | 23 | 24 | ## Project Status 25 | 26 | This project is quite stable and ready for production use, however there are plans to improve it including: 27 | 28 | - CSS Styles and theme support (working, could be improved) 29 | - Documentation website (currently just examples) 30 | - Custom options rendering (in progress) 31 | 32 | It's loosely based on [Selectize](http://brianreavis.github.io/selectize.js/) (in terms of behaviour and user experience) and [React-Autocomplete](https://github.com/rackt/react-autocomplete) (as a native React Combobox implementation), as well as other select controls including [Chosen](http://harvesthq.github.io/chosen/) and [Select2](http://ivaynberg.github.io/select2/). 33 | 34 | 35 | ## Installation 36 | 37 | The easiest way to use React-Select is to install it from NPM and include it in your own React build process (using [Browserify](http://browserify.org), etc). 38 | 39 | ``` 40 | npm install react-select --save 41 | ``` 42 | 43 | You can also use the standalone build by including `dist/select.js` and `dist/default.css` in your page. If you use this, make sure you have already included the following dependencies: 44 | 45 | * [React](http://facebook.github.io/react/) 46 | * [classNames](http://jedwatson.github.io/classnames/) 47 | * [react-input-autosize](https://github.com/JedWatson/react-input-autosize) 48 | 49 | 50 | ## Usage 51 | 52 | React-Select generates a hidden text field containing the selected value, so you can submit it as part of a standard form. You can also listen for changes with the `onChange` event property. 53 | 54 | Options should be provided as an `Array` of `Object`s, each with a `value` and `label` property for rendering and searching. You can use a `disabled` property to indicate whether the option is disabled or not. 55 | 56 | The `value` property of each option should be set to either a string or a number. 57 | 58 | When the value is changed, `onChange(newValue, [selectedOptions])` will fire. 59 | 60 | ``` 61 | var Select = require('react-select'); 62 | 63 | var options = [ 64 | { value: 'one', label: 'One' }, 65 | { value: 'two', label: 'Two' } 66 | ]; 67 | 68 | function logChange(val) { 69 | console.log("Selected: " + val); 70 | } 71 | 72 | 125 | ``` 126 | 127 | ### Async options loaded externally 128 | 129 | If you want to load options asynchronously externally from the `Select` component, you can have the `Select` component show a loading spinner by passing in the `isLoading` prop set to `true`. 130 | 131 | ``` 132 | var Select = require('react-select'); 133 | 134 | var isLoadingExternally = true; 135 | 136 | tag 189 | newOptionCreator | func | factory to create new options when allowCreate set 190 | noResultsText | string | placeholder displayed when there are no matching search results 191 | onBlur | func | onBlur handler: function(event) {} 192 | onChange | func | onChange handler: function(newValue) {} 193 | onFocus | func | onFocus handler: function(event) {} 194 | onInputChange | func | onInputChange handler: function(inputValue) {} 195 | onOptionLabelClick | func | onCLick handler for value labels: function (value, event) {} 196 | optionRenderer | func | function which returns a custom way to render the options in the menu 197 | options | array | array of options 198 | placeholder | string | field placeholder, displayed when there's no value 199 | searchable | bool | whether to enable searching feature or not 200 | searchingText | string | message to display whilst options are loading via asyncOptions, or when isLoading prop is `true` 201 | searchPromptText | string | label to prompt for search input 202 | value | any | initial field value 203 | valueRenderer | func | function which returns a custom way to render the value selected 204 | 205 | ### Methods 206 | 207 | Right now there's simply a `focus()` method that gives the control focus. All other methods on `` component, thanks [Yann Plantevin](https://github.com/YannPl) 181 | * changed; now using [react-component-gulp-tasks](https://github.com/JedWatson/react-component-gulp-tasks) for build 182 | * fixed; issue w/ remote callbacks overriding cached options, thanks [Corey McMahon](https://github.com/coreymcmahon) 183 | * fixed; if not `this.props.multi`, menu doesn't need handleMouseDown, thanks [wenbing](https://github.com/wenbing) 184 | 185 | ## v0.3.4 / 2015-02-23 186 | 187 | * fixed; issues with the underscore/lodash dependency change, thanks [Aaron Powell](https://github.com/aaronpowell) 188 | 189 | ## v0.3.3 / 2015-02-22 190 | 191 | * added; `disabled` prop, thanks [Danny Shaw](https://github.com/dannyshaw) 192 | * added; `searchable` prop - set to `false` to disable the search box, thanks [Julen Ruiz Aizpuru](https://github.com/julen) 193 | * added; `onOptionLabelClick` prop - see [#66](https://github.com/JedWatson/react-select/pull/66) for docs, thanks [Dmitry Smirnov](https://github.com/dmitry-smirnov) 194 | * fixed; `text-overflow: ellipsis;` typo, thanks [Andru Vallance](https://github.com/andru) 195 | 196 | ## v0.3.2 / 2015-01-30 197 | 198 | * fixed; issue adding undefined values to multiselect, thanks [Tejas Dinkar](https://github.com/gja) 199 | 200 | ## v0.3.1 / 2015-01-20 201 | 202 | * fixed; missing `var` statement 203 | 204 | ## v0.3.0 / 2015-01-20 205 | 206 | * added; node compatible build now available in `/lib` 207 | 208 | ## v0.2.14 / 2015-01-07 209 | 210 | * added; `searchPromptText` property that is displayed when `asyncOptions` is set and there are (a) no options loaded, and (b) no input entered to search on, thanks [Anton Fedchenko](https://github.com/kompot) 211 | * added; `clearable` property (defaults to `true`) to control whether the "clear" control is available, thanks [Anton Fedchenko](https://github.com/kompot) 212 | 213 | ## v0.2.13 / 2015-01-05 214 | 215 | * fixed; height issues in Safari, thanks [Joss Mackison](https://github.com/jossmac) 216 | * added; Option to specify "Clear value" label as prop for i18n 217 | 218 | ## v0.2.12 / 2015-01-04 219 | 220 | * fixed; UI now responds to touch events, and works on mobile devices! thanks [Fraser Xu](https://github.com/fraserxu) 221 | 222 | ## v0.2.11 / 2015-01-04 223 | 224 | * fixed; Options in the dropdown now scroll into view when they are focused, thanks [Adam](https://github.com/fmovlex) 225 | * improved; Example dist is now excluded from the npm package 226 | 227 | ## v0.2.10 / 2015-01-01 228 | 229 | * fixed; More specific mixin name to avoid conflicts (css) 230 | * fixed; Example CSS now correctly rebuilds on changes in development 231 | * fixed; Values are now expanded correctly when options change (see #28) 232 | * added; Option to specify "No results found" label as prop for i18n, thanks [Julen Ruiz Aizpuru](https://github.com/julen) 233 | 234 | ## v0.2.9 / 2014-12-09 235 | 236 | * added; `filterOption` and `filterOptions` props for more control over filtering 237 | 238 | ## v0.2.8 / 2014-12-08 239 | 240 | * added; `matchPos` option to control whether to match the `start` or `any` position in the string when filtering options (default: `any`) 241 | * added; `matchProp` option to control whether to match the `value`, `label` or `any` property of each option when filtering (default: `any`) 242 | 243 | ## v0.2.7 / 2014-12-01 244 | 245 | * fixed; screen-readers will now read "clear value" instead of "times" for the clear button 246 | * fixed; non-left-click mousedown events aren't blocked by the control 247 | 248 | 249 | ## v0.2.6 / 2014-11-30 250 | 251 | * improved; better comparison of changes to [options] in `willReceiveProps` 252 | * fixed; now focuses the first option correctly when in multiselect mode 253 | * fixed; fixed focused option behaviour on value change 254 | * fixed; when filtering, there is always a focused option (#19) 255 | * changed; using ^ in package.json to compare dependencies 256 | 257 | ## v0.2.5 / 2014-11-20 258 | 259 | * fixed; compatibility with case-sensitive file systems 260 | 261 | ## v0.2.4 / 2014-11-20 262 | 263 | * fixed; package.json pointed at the right file 264 | 265 | ## v0.2.3 / 2014-11-17 266 | 267 | * fixed; Updating state correctly when props change 268 | * improved; Build tasks and docs 269 | * added; Working standalone build 270 | * added; Minified dist version 271 | * added; Publised to Bower 272 | 273 | ## v0.2.2 / 2014-11-15 274 | 275 | * fixed; backspace event being incorrectly cancelled 276 | 277 | ## v0.2.1 / 2014-11-15 278 | 279 | * fixed; issue where backspace incorrectly clears the value (#14) 280 | 281 | ## v0.2.0 / 2014-11-15 282 | 283 | * changed; Major rewrite to improve focus handling and internal state management 284 | * added; Support for `multi` prop, enable multiselect mode 285 | 286 | ## v0.1.1 / 2014-11-03 287 | 288 | * added; Support for `onChange` event 289 | * added; `propTypes` are defined by the `Select` component now 290 | * added; `className` property, sets the `className` on the outer `div` element 291 | * fixed; Removed deprecated `React.DOM.x` calls 292 | 293 | ## v0.1.0 / 2014-11-01 294 | 295 | * updated; React to 0.12.0 296 | 297 | ## v0.0.6 / 2014-10-14 298 | 299 | * fixed; Error keeping value when using Async Options 300 | -------------------------------------------------------------------------------- /dist/react-select.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.Select=e()}}(function(){return function e(t,s,i){function o(a,p){if(!s[a]){if(!t[a]){var r="function"==typeof require&&require;if(!p&&r)return r(a,!0);if(n)return n(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var u=s[a]={exports:{}};t[a][0].call(u.exports,function(e){var s=t[a][1][e];return o(s?s:e)},u,u.exports,e,t,s,i)}return s[a].exports}for(var n="function"==typeof require&&require,a=0;an.bottom||i.top-1)return!1;if(this.props.filterOption)return this.props.filterOption.call(this,e,s);var t=String(e.value),o=String(e.label);return this.props.ignoreCase&&(t=t.toLowerCase(),o=o.toLowerCase(),s=s.toLowerCase()),s&&"start"!==this.props.matchPos?"label"!==this.props.matchProp&&t.indexOf(s)>=0||"value"!==this.props.matchProp&&o.indexOf(s)>=0:"label"!==this.props.matchProp&&t.substr(0,s.length)===s||"value"!==this.props.matchProp&&o.substr(0,s.length)===s};return(e||[]).filter(o,this)},selectFocusedOption:function(){return this.props.allowCreate&&!this.state.focusedOption?this.selectValue(this.state.inputValue):this.state.focusedOption?this.selectValue(this.state.focusedOption):void 0},focusOption:function(e){this.setState({focusedOption:e})},focusNextOption:function(){this.focusAdjacentOption("next")},focusPreviousOption:function(){this.focusAdjacentOption("previous")},focusAdjacentOption:function(e){this._focusedOptionReveal=!0;var t=this.state.filteredOptions.filter(function(e){return!e.disabled});if(!this.state.isOpen)return void this.setState({isOpen:!0,inputValue:"",focusedOption:this.state.focusedOption||t["next"===e?0:t.length-1]},this._bindCloseMenuIfClickedOutside);if(t.length){for(var s=-1,i=0;i-1&&s0?t[s-1]:t[t.length-1]),this.setState({focusedOption:o})}},unfocusOption:function(e){this.state.focusedOption===e&&this.setState({focusedOption:null})},buildMenu:function(){var e=this.state.focusedOption?this.state.focusedOption.value:null,t=this.props.optionRenderer||function(e){return e.label};this.state.filteredOptions.length>0&&(e=null==e?this.state.filteredOptions[0]:e);var s=this.state.filteredOptions;if(this.props.allowCreate&&this.state.inputValue.trim()){var i=this.state.inputValue;s=s.slice();var n=this.props.newOptionCreator?this.props.newOptionCreator(i):{value:i,label:i,create:!0};s.unshift(n)}var p=Object.keys(s).map(function(i){var n=s[i],p=this.state.value===n.value,r=e===n.value,l=a({"Select-option":!0,"is-selected":p,"is-focused":r,"is-disabled":n.disabled}),u=r?"focused":null,c=this.focusOption.bind(this,n),h=this.unfocusOption.bind(this,n),d=this.selectValue.bind(this,n),f=o.createElement(this.props.optionComponent,{key:"option-"+n.value,className:l,renderFunc:t,mouseEnter:c,mouseLeave:h,mouseDown:d,click:d,addLabelText:this.props.addLabelText,option:n,ref:u});return f},this);if(p.length)return p;var r,l;return this.isLoading()?(l="Select-searching",r=this.props.searchingText):this.state.inputValue||!this.props.asyncOptions?(l="Select-noresults",r=this.props.noResultsText):(l="Select-search-prompt",r=this.props.searchPromptText),o.createElement("div",{className:l},r)},handleOptionLabelClick:function(e,t){this.props.onOptionLabelClick&&this.props.onOptionLabelClick(e,t)},isLoading:function(){return this.props.isLoading||this.state.isLoading},render:function(){var e=a("Select",this.props.className,{"is-multi":this.props.multi,"is-searchable":this.props.searchable,"is-open":this.state.isOpen,"is-focused":this.state.isFocused,"is-loading":this.isLoading(),"is-disabled":this.props.disabled,"has-value":this.state.value}),t=[];if(this.props.multi&&this.state.values.forEach(function(e){var s=this.handleOptionLabelClick.bind(this,e),i=this.removeValue.bind(this,e),n=o.createElement(this.props.valueComponent,{key:e.value,option:e,renderer:this.props.valueRenderer,optionLabelClick:!!this.props.onOptionLabelClick,onOptionLabelClick:s,onRemove:i,disabled:this.props.disabled});t.push(n)},this),!(this.state.inputValue||this.props.multi&&t.length)){var s=this.state.values[0]||null;if(this.props.valueRenderer&&this.state.values.length)t.push(o.createElement(p,{key:0,option:s,renderer:this.props.valueRenderer,disabled:this.props.disabled}));else{var r=o.createElement(this.props.singleValueComponent,{key:"placeholder",value:s,placeholder:this.state.placeholder});t.push(r)}}var l,u,c=this.isLoading()?o.createElement("span",{className:"Select-loading","aria-hidden":"true"}):null,h=this.props.clearable&&this.state.value&&!this.props.disabled?o.createElement("span",{className:"Select-clear",title:this.props.multi?this.props.clearAllText:this.props.clearValueText,"aria-label":this.props.multi?this.props.clearAllText:this.props.clearValueText,onMouseDown:this.clearValue,onTouchEnd:this.clearValue,onClick:this.clearValue,dangerouslySetInnerHTML:{__html:"×"}}):null;this.state.isOpen&&(u={ref:"menu",className:"Select-menu",onMouseDown:this.handleMouseDown},l=o.createElement("div",{ref:"selectMenuContainer",className:"Select-menu-outer"},o.createElement("div",u,this.buildMenu())));var d,f={ref:"input",className:"Select-input "+(this.props.inputProps.className||""),tabIndex:this.props.tabIndex||0,onFocus:this.handleInputFocus,onBlur:this.handleInputBlur};for(var v in this.props.inputProps)this.props.inputProps.hasOwnProperty(v)&&"className"!==v&&(f[v]=this.props.inputProps[v]);return this.props.disabled?this.props.multi&&this.state.values.length||(d=o.createElement("div",{className:"Select-input"}," ")):d=this.props.searchable?o.createElement(n,i({value:this.state.inputValue,onChange:this.handleInputChange,minWidth:"5"},f)):o.createElement("div",f," "),o.createElement("div",{ref:"wrapper",className:e},o.createElement("input",{type:"hidden",ref:"value",name:this.props.name,value:this.state.value,disabled:this.props.disabled}),o.createElement("div",{className:"Select-control",ref:"control",onKeyDown:this.handleKeyDown,onMouseDown:this.handleMouseDown,onTouchEnd:this.handleMouseDown},t,d,o.createElement("span",{className:"Select-arrow-zone",onMouseDown:this.handleMouseDownOnArrow}),o.createElement("span",{className:"Select-arrow",onMouseDown:this.handleMouseDownOnArrow}),c,h),l)}});t.exports=c}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./Option":1,"./SingleValue":3,"./Value":4}],3:[function(e,t,s){(function(e){"use strict";var s="undefined"!=typeof window?window.React:"undefined"!=typeof e?e.React:null,i="undefined"!=typeof window?window.classNames:"undefined"!=typeof e?e.classNames:null,o=s.createClass({displayName:"SingleValue",propTypes:{placeholder:s.PropTypes.string,value:s.PropTypes.object},render:function(){var e=i("Select-placeholder",this.props.value&&this.props.value.className);return s.createElement("div",{className:e,style:this.props.value&&this.props.value.style,title:this.props.value&&this.props.value.title},this.props.placeholder)}});t.exports=o}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],4:[function(e,t,s){(function(e){"use strict";var s="undefined"!=typeof window?window.React:"undefined"!=typeof e?e.React:null,i="undefined"!=typeof window?window.classNames:"undefined"!=typeof e?e.classNames:null,o=s.createClass({displayName:"Value",propTypes:{disabled:s.PropTypes.bool,onOptionLabelClick:s.PropTypes.func,onRemove:s.PropTypes.func,option:s.PropTypes.object.isRequired,optionLabelClick:s.PropTypes.bool,renderer:s.PropTypes.func},blockEvent:function(e){e.stopPropagation()},handleOnRemove:function(e){this.props.disabled||this.props.onRemove(e)},render:function(){var e=this.props.option.label;return this.props.renderer&&(e=this.props.renderer(this.props.option)),this.props.onRemove||this.props.optionLabelClick?(this.props.optionLabelClick&&(e=s.createElement("a",{className:i("Select-item-label__a",this.props.option.className),onMouseDown:this.blockEvent,onTouchEnd:this.props.onOptionLabelClick,onClick:this.props.onOptionLabelClick,style:this.props.option.style,title:this.props.option.title},e)),s.createElement("div",{className:i("Select-item",this.props.option.className),style:this.props.option.style,title:this.props.option.title},s.createElement("span",{className:"Select-item-icon",onMouseDown:this.blockEvent,onClick:this.handleOnRemove,onTouchEnd:this.handleOnRemove},"×"),s.createElement("span",{className:"Select-item-label"},e))):s.createElement("div",{className:i("Select-value",this.props.option.className),style:this.props.option.style,title:this.props.option.title},e)}});t.exports=o}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[2])(2)}); -------------------------------------------------------------------------------- /src/Select.js: -------------------------------------------------------------------------------- 1 | /* disable some rules until we refactor more completely; fixing them now would 2 | cause conflicts with some open PRs unnecessarily. */ 3 | /* eslint react/jsx-sort-prop-types: 0, react/sort-comp: 0, react/prop-types: 0 */ 4 | 5 | var React = require('react'); 6 | var Input = require('react-input-autosize'); 7 | var classes = require('classnames'); 8 | var Value = require('./Value'); 9 | var SingleValue = require('./SingleValue'); 10 | var Option = require('./Option'); 11 | 12 | var requestId = 0; 13 | 14 | var Select = React.createClass({ 15 | 16 | displayName: 'Select', 17 | 18 | propTypes: { 19 | addLabelText: React.PropTypes.string, // placeholder displayed when you want to add a label on a multi-value input 20 | allowCreate: React.PropTypes.bool, // whether to allow creation of new entries 21 | asyncOptions: React.PropTypes.func, // function to call to get options 22 | autoload: React.PropTypes.bool, // whether to auto-load the default async options set 23 | backspaceRemoves: React.PropTypes.bool, // whether backspace removes an item if there is no text input 24 | cacheAsyncResults: React.PropTypes.bool, // whether to allow cache 25 | className: React.PropTypes.string, // className for the outer element 26 | clearAllText: React.PropTypes.string, // title for the "clear" control when multi: true 27 | clearValueText: React.PropTypes.string, // title for the "clear" control 28 | clearable: React.PropTypes.bool, // should it be possible to reset value 29 | delimiter: React.PropTypes.string, // delimiter to use to join multiple values 30 | disabled: React.PropTypes.bool, // whether the Select is disabled or not 31 | filterOption: React.PropTypes.func, // method to filter a single option: function(option, filterString) 32 | filterOptions: React.PropTypes.func, // method to filter the options array: function([options], filterString, [values]) 33 | ignoreCase: React.PropTypes.bool, // whether to perform case-insensitive filtering 34 | inputProps: React.PropTypes.object, // custom attributes for the Input (in the Select-control) e.g: {'data-foo': 'bar'} 35 | isLoading: React.PropTypes.bool, // whether the Select is loading externally or not (such as options being loaded) 36 | matchPos: React.PropTypes.string, // (any|start) match the start or entire string when filtering 37 | matchProp: React.PropTypes.string, // (any|label|value) which option property to filter on 38 | multi: React.PropTypes.bool, // multi-value input 39 | name: React.PropTypes.string, // field name, for hidden tag 40 | newOptionCreator: React.PropTypes.func, // factory to create new options when allowCreate set 41 | noResultsText: React.PropTypes.string, // placeholder displayed when there are no matching search results 42 | onBlur: React.PropTypes.func, // onBlur handler: function(event) {} 43 | onChange: React.PropTypes.func, // onChange handler: function(newValue) {} 44 | onFocus: React.PropTypes.func, // onFocus handler: function(event) {} 45 | onInputChange: React.PropTypes.func, // onInputChange handler: function(inputValue) {} 46 | onOptionLabelClick: React.PropTypes.func, // onCLick handler for value labels: function (value, event) {} 47 | optionComponent: React.PropTypes.func, // option component to render in dropdown 48 | optionRenderer: React.PropTypes.func, // optionRenderer: function(option) {} 49 | options: React.PropTypes.array, // array of options 50 | placeholder: React.PropTypes.string, // field placeholder, displayed when there's no value 51 | searchable: React.PropTypes.bool, // whether to enable searching feature or not 52 | searchingText: React.PropTypes.string, // message to display whilst options are loading via asyncOptions 53 | searchPromptText: React.PropTypes.string, // label to prompt for search input 54 | singleValueComponent: React.PropTypes.func,// single value component when multiple is set to false 55 | value: React.PropTypes.any, // initial field value 56 | valueComponent: React.PropTypes.func, // value component to render in multiple mode 57 | valueRenderer: React.PropTypes.func // valueRenderer: function(option) {} 58 | }, 59 | 60 | getDefaultProps: function() { 61 | return { 62 | addLabelText: 'Add "{label}"?', 63 | allowCreate: false, 64 | asyncOptions: undefined, 65 | autoload: true, 66 | backspaceRemoves: true, 67 | cacheAsyncResults: true, 68 | className: undefined, 69 | clearAllText: 'Clear all', 70 | clearValueText: 'Clear value', 71 | clearable: true, 72 | delimiter: ',', 73 | disabled: false, 74 | ignoreCase: true, 75 | inputProps: {}, 76 | isLoading: false, 77 | matchPos: 'any', 78 | matchProp: 'any', 79 | name: undefined, 80 | newOptionCreator: undefined, 81 | noResultsText: 'No results found', 82 | onChange: undefined, 83 | onInputChange: undefined, 84 | onOptionLabelClick: undefined, 85 | optionComponent: Option, 86 | options: undefined, 87 | placeholder: 'Select...', 88 | searchable: true, 89 | searchingText: 'Searching...', 90 | searchPromptText: 'Type to search', 91 | singleValueComponent: SingleValue, 92 | value: undefined, 93 | valueComponent: Value 94 | }; 95 | }, 96 | 97 | getInitialState: function() { 98 | return { 99 | /* 100 | * set by getStateFromValue on componentWillMount: 101 | * - value 102 | * - values 103 | * - filteredOptions 104 | * - inputValue 105 | * - placeholder 106 | * - focusedOption 107 | */ 108 | isFocused: false, 109 | isLoading: false, 110 | isOpen: false, 111 | options: this.props.options 112 | }; 113 | }, 114 | 115 | componentWillMount: function() { 116 | this._optionsCache = {}; 117 | this._optionsFilterString = ''; 118 | this._closeMenuIfClickedOutside = (event) => { 119 | if (!this.state.isOpen) { 120 | return; 121 | } 122 | var menuElem = React.findDOMNode(this.refs.selectMenuContainer); 123 | var controlElem = React.findDOMNode(this.refs.control); 124 | 125 | var eventOccuredOutsideMenu = this.clickedOutsideElement(menuElem, event); 126 | var eventOccuredOutsideControl = this.clickedOutsideElement(controlElem, event); 127 | 128 | // Hide dropdown menu if click occurred outside of menu 129 | if (eventOccuredOutsideMenu && eventOccuredOutsideControl) { 130 | this.setState({ 131 | isOpen: false 132 | }, this._unbindCloseMenuIfClickedOutside); 133 | } 134 | }; 135 | this._bindCloseMenuIfClickedOutside = function() { 136 | if (!document.addEventListener && document.attachEvent) { 137 | document.attachEvent('onclick', this._closeMenuIfClickedOutside); 138 | } else { 139 | document.addEventListener('click', this._closeMenuIfClickedOutside); 140 | } 141 | }; 142 | this._unbindCloseMenuIfClickedOutside = function() { 143 | if (!document.removeEventListener && document.detachEvent) { 144 | document.detachEvent('onclick', this._closeMenuIfClickedOutside); 145 | } else { 146 | document.removeEventListener('click', this._closeMenuIfClickedOutside); 147 | } 148 | }; 149 | this.setState(this.getStateFromValue(this.props.value)); 150 | }, 151 | 152 | componentDidMount: function() { 153 | if (this.props.asyncOptions && this.props.autoload) { 154 | this.autoloadAsyncOptions(); 155 | } 156 | }, 157 | 158 | componentWillUnmount: function() { 159 | clearTimeout(this._blurTimeout); 160 | clearTimeout(this._focusTimeout); 161 | if (this.state.isOpen) { 162 | this._unbindCloseMenuIfClickedOutside(); 163 | } 164 | }, 165 | 166 | componentWillReceiveProps: function(newProps) { 167 | var optionsChanged = false; 168 | if (JSON.stringify(newProps.options) !== JSON.stringify(this.props.options)) { 169 | optionsChanged = true; 170 | this.setState({ 171 | options: newProps.options, 172 | filteredOptions: this.filterOptions(newProps.options) 173 | }); 174 | } 175 | if (newProps.value !== this.state.value || newProps.placeholder !== this.props.placeholder || optionsChanged) { 176 | var setState = (newState) => { 177 | this.setState(this.getStateFromValue(newProps.value, 178 | (newState && newState.options) || newProps.options, 179 | newProps.placeholder) 180 | ); 181 | }; 182 | if (this.props.asyncOptions) { 183 | this.loadAsyncOptions(newProps.value, {}, setState); 184 | } else { 185 | setState(); 186 | } 187 | } 188 | }, 189 | 190 | componentDidUpdate: function() { 191 | if (!this.props.disabled && this._focusAfterUpdate) { 192 | clearTimeout(this._blurTimeout); 193 | this._focusTimeout = setTimeout(() => { 194 | this.getInputNode().focus(); 195 | this._focusAfterUpdate = false; 196 | }, 50); 197 | } 198 | if (this._focusedOptionReveal) { 199 | if (this.refs.focused && this.refs.menu) { 200 | var focusedDOM = React.findDOMNode(this.refs.focused); 201 | var menuDOM = React.findDOMNode(this.refs.menu); 202 | var focusedRect = focusedDOM.getBoundingClientRect(); 203 | var menuRect = menuDOM.getBoundingClientRect(); 204 | 205 | if (focusedRect.bottom > menuRect.bottom || focusedRect.top < menuRect.top) { 206 | menuDOM.scrollTop = (focusedDOM.offsetTop + focusedDOM.clientHeight - menuDOM.offsetHeight); 207 | } 208 | } 209 | this._focusedOptionReveal = false; 210 | } 211 | }, 212 | 213 | focus: function() { 214 | this.getInputNode().focus(); 215 | }, 216 | 217 | clickedOutsideElement: function(element, event) { 218 | var eventTarget = (event.target) ? event.target : event.srcElement; 219 | while (eventTarget != null) { 220 | if (eventTarget === element) return false; 221 | eventTarget = eventTarget.offsetParent; 222 | } 223 | return true; 224 | }, 225 | 226 | getStateFromValue: function(value, options, placeholder) { 227 | if (!options) { 228 | options = this.state.options; 229 | } 230 | if (!placeholder) { 231 | placeholder = this.props.placeholder; 232 | } 233 | 234 | // reset internal filter string 235 | this._optionsFilterString = ''; 236 | 237 | var values = this.initValuesArray(value, options); 238 | var filteredOptions = this.filterOptions(options, values); 239 | 240 | var focusedOption; 241 | var valueForState = null; 242 | if (!this.props.multi && values.length) { 243 | focusedOption = values[0]; 244 | valueForState = values[0].value; 245 | } else { 246 | focusedOption = this.getFirstFocusableOption(filteredOptions); 247 | valueForState = values.map(function(v) { return v.value; }).join(this.props.delimiter); 248 | } 249 | 250 | return { 251 | value: valueForState, 252 | values: values, 253 | inputValue: '', 254 | filteredOptions: filteredOptions, 255 | placeholder: !this.props.multi && values.length ? values[0].label : placeholder, 256 | focusedOption: focusedOption 257 | }; 258 | }, 259 | 260 | getFirstFocusableOption: function (options) { 261 | 262 | for (var optionIndex = 0; optionIndex < options.length; ++optionIndex) { 263 | if (!options[optionIndex].disabled) { 264 | return options[optionIndex]; 265 | } 266 | } 267 | }, 268 | 269 | initValuesArray: function(values, options) { 270 | if (!Array.isArray(values)) { 271 | if (typeof values === 'string') { 272 | values = values === '' 273 | ? [] 274 | : this.props.multi 275 | ? values.split(this.props.delimiter) 276 | : [ values ]; 277 | } else { 278 | values = values !== undefined && values !== null ? [values] : []; 279 | } 280 | } 281 | return values.map(function(val) { 282 | if (typeof val === 'string' || typeof val === 'number') { 283 | for (var key in options) { 284 | if (options.hasOwnProperty(key) && 285 | options[key] && 286 | (options[key].value === val || 287 | typeof options[key].value === 'number' && 288 | options[key].value.toString() === val 289 | )) { 290 | return options[key]; 291 | } 292 | } 293 | return { value: val, label: val }; 294 | } else { 295 | return val; 296 | } 297 | }); 298 | }, 299 | 300 | setValue: function(value, focusAfterUpdate) { 301 | if (focusAfterUpdate || focusAfterUpdate === undefined) { 302 | this._focusAfterUpdate = true; 303 | } 304 | var newState = this.getStateFromValue(value); 305 | newState.isOpen = false; 306 | this.fireChangeEvent(newState); 307 | this.setState(newState); 308 | }, 309 | 310 | selectValue: function(value) { 311 | if (!this.props.multi) { 312 | this.setValue(value); 313 | } else if (value) { 314 | this.addValue(value); 315 | } 316 | this._unbindCloseMenuIfClickedOutside(); 317 | }, 318 | 319 | addValue: function(value) { 320 | this.setValue(this.state.values.concat(value)); 321 | }, 322 | 323 | popValue: function() { 324 | this.setValue(this.state.values.slice(0, this.state.values.length - 1)); 325 | }, 326 | 327 | removeValue: function(valueToRemove) { 328 | this.setValue(this.state.values.filter(function(value) { 329 | return value !== valueToRemove; 330 | })); 331 | }, 332 | 333 | clearValue: function(event) { 334 | // if the event was triggered by a mousedown and not the primary 335 | // button, ignore it. 336 | if (event && event.type === 'mousedown' && event.button !== 0) { 337 | return; 338 | } 339 | event.stopPropagation(); 340 | event.preventDefault(); 341 | this.setValue(null); 342 | }, 343 | 344 | resetValue: function() { 345 | this.setValue(this.state.value === '' ? null : this.state.value); 346 | }, 347 | 348 | getInputNode: function () { 349 | var input = this.refs.input; 350 | return this.props.searchable ? input : React.findDOMNode(input); 351 | }, 352 | 353 | fireChangeEvent: function(newState) { 354 | if (newState.value !== this.state.value && this.props.onChange) { 355 | this.props.onChange(newState.value, newState.values); 356 | } 357 | }, 358 | 359 | handleMouseDown: function(event) { 360 | // if the event was triggered by a mousedown and not the primary 361 | // button, or if the component is disabled, ignore it. 362 | if (this.props.disabled || (event.type === 'mousedown' && event.button !== 0)) { 363 | return; 364 | } 365 | event.stopPropagation(); 366 | event.preventDefault(); 367 | 368 | // for the non-searchable select, close the dropdown when button is clicked 369 | if (this.state.isOpen && !this.props.searchable) { 370 | this.setState({ 371 | isOpen: false 372 | }, this._unbindCloseMenuIfClickedOutside); 373 | return; 374 | } 375 | 376 | if (this.state.isFocused) { 377 | this.setState({ 378 | isOpen: true 379 | }, this._bindCloseMenuIfClickedOutside); 380 | } else { 381 | this._openAfterFocus = true; 382 | this.getInputNode().focus(); 383 | } 384 | }, 385 | 386 | handleMouseDownOnArrow: function(event) { 387 | // if the event was triggered by a mousedown and not the primary 388 | // button, or if the component is disabled, ignore it. 389 | if (this.props.disabled || (event.type === 'mousedown' && event.button !== 0)) { 390 | return; 391 | } 392 | // If not focused, handleMouseDown will handle it 393 | if (!this.state.isOpen) { 394 | return; 395 | } 396 | event.stopPropagation(); 397 | event.preventDefault(); 398 | this.setState({ 399 | isOpen: false 400 | }, this._unbindCloseMenuIfClickedOutside); 401 | }, 402 | 403 | handleInputFocus: function(event) { 404 | var newIsOpen = this.state.isOpen || this._openAfterFocus; 405 | this.setState({ 406 | isFocused: true, 407 | isOpen: newIsOpen 408 | }, function() { 409 | if(newIsOpen) { 410 | this._bindCloseMenuIfClickedOutside(); 411 | } 412 | else { 413 | this._unbindCloseMenuIfClickedOutside(); 414 | } 415 | }); 416 | this._openAfterFocus = false; 417 | if (this.props.onFocus) { 418 | this.props.onFocus(event); 419 | } 420 | }, 421 | 422 | handleInputBlur: function(event) { 423 | this._blurTimeout = setTimeout(() => { 424 | if (this._focusAfterUpdate) return; 425 | this.setState({ 426 | isFocused: false, 427 | isOpen: false 428 | }); 429 | }, 50); 430 | if (this.props.onBlur) { 431 | this.props.onBlur(event); 432 | } 433 | }, 434 | 435 | handleKeyDown: function(event) { 436 | if (this.props.disabled) return; 437 | switch (event.keyCode) { 438 | case 8: // backspace 439 | if (!this.state.inputValue && this.props.backspaceRemoves) { 440 | event.preventDefault(); 441 | this.popValue(); 442 | } 443 | return; 444 | case 9: // tab 445 | if (event.shiftKey || !this.state.isOpen || !this.state.focusedOption) { 446 | return; 447 | } 448 | this.selectFocusedOption(); 449 | break; 450 | case 13: // enter 451 | if (!this.state.isOpen) return; 452 | 453 | this.selectFocusedOption(); 454 | break; 455 | case 27: // escape 456 | if (this.state.isOpen) { 457 | this.resetValue(); 458 | } else if (this.props.clearable) { 459 | this.clearValue(event); 460 | } 461 | break; 462 | case 38: // up 463 | this.focusPreviousOption(); 464 | break; 465 | case 40: // down 466 | this.focusNextOption(); 467 | break; 468 | case 188: // , 469 | if (this.props.allowCreate && this.props.multi) { 470 | event.preventDefault(); 471 | event.stopPropagation(); 472 | this.selectFocusedOption(); 473 | } else { 474 | return; 475 | } 476 | break; 477 | default: return; 478 | } 479 | event.preventDefault(); 480 | }, 481 | 482 | // Ensures that the currently focused option is available in filteredOptions. 483 | // If not, returns the first available option. 484 | _getNewFocusedOption: function(filteredOptions) { 485 | for (var key in filteredOptions) { 486 | if (filteredOptions.hasOwnProperty(key) && filteredOptions[key] === this.state.focusedOption) { 487 | return filteredOptions[key]; 488 | } 489 | } 490 | return this.getFirstFocusableOption(filteredOptions); 491 | }, 492 | 493 | handleInputChange: function(event) { 494 | // assign an internal variable because we need to use 495 | // the latest value before setState() has completed. 496 | this._optionsFilterString = event.target.value; 497 | 498 | if (this.props.onInputChange) { 499 | this.props.onInputChange(event.target.value); 500 | } 501 | 502 | if (this.props.asyncOptions) { 503 | this.setState({ 504 | isLoading: true, 505 | inputValue: event.target.value 506 | }); 507 | this.loadAsyncOptions(event.target.value, { 508 | isLoading: false, 509 | isOpen: true 510 | }, this._bindCloseMenuIfClickedOutside); 511 | } else { 512 | var filteredOptions = this.filterOptions(this.state.options); 513 | this.setState({ 514 | isOpen: true, 515 | inputValue: event.target.value, 516 | filteredOptions: filteredOptions, 517 | focusedOption: this._getNewFocusedOption(filteredOptions) 518 | }, this._bindCloseMenuIfClickedOutside); 519 | } 520 | }, 521 | 522 | autoloadAsyncOptions: function() { 523 | this.setState({ 524 | isLoading: true 525 | }); 526 | this.loadAsyncOptions((this.props.value || ''), { isLoading: false }, () => { 527 | // update with fetched but don't focus 528 | this.setValue(this.props.value, false); 529 | }); 530 | }, 531 | 532 | loadAsyncOptions: function(input, state, callback) { 533 | var thisRequestId = this._currentRequestId = requestId++; 534 | if (this.props.cacheAsyncResults) { 535 | for (var i = 0; i <= input.length; i++) { 536 | var cacheKey = input.slice(0, i); 537 | if (this._optionsCache[cacheKey] && (input === cacheKey || this._optionsCache[cacheKey].complete)) { 538 | var options = this._optionsCache[cacheKey].options; 539 | var filteredOptions = this.filterOptions(options); 540 | var newState = { 541 | options: options, 542 | filteredOptions: filteredOptions, 543 | focusedOption: this._getNewFocusedOption(filteredOptions) 544 | }; 545 | for (var key in state) { 546 | if (state.hasOwnProperty(key)) { 547 | newState[key] = state[key]; 548 | } 549 | } 550 | this.setState(newState); 551 | if (callback) callback.call(this, newState); 552 | return; 553 | } 554 | } 555 | } 556 | 557 | this.props.asyncOptions(input, (err, data) => { 558 | if (err) throw err; 559 | if (this.props.cacheAsyncResults) { 560 | this._optionsCache[input] = data; 561 | } 562 | if (thisRequestId !== this._currentRequestId) { 563 | return; 564 | } 565 | var filteredOptions = this.filterOptions(data.options); 566 | var newState = { 567 | options: data.options, 568 | filteredOptions: filteredOptions, 569 | focusedOption: this._getNewFocusedOption(filteredOptions) 570 | }; 571 | for (var key in state) { 572 | if (state.hasOwnProperty(key)) { 573 | newState[key] = state[key]; 574 | } 575 | } 576 | this.setState(newState); 577 | if (callback) callback.call(this, newState); 578 | }); 579 | }, 580 | 581 | filterOptions: function(options, values) { 582 | var filterValue = this._optionsFilterString; 583 | var exclude = (values || this.state.values).map(function(i) { 584 | return i.value; 585 | }); 586 | if (this.props.filterOptions) { 587 | return this.props.filterOptions.call(this, options, filterValue, exclude); 588 | } else { 589 | var filterOption = function(op) { 590 | if (this.props.multi && exclude.indexOf(op.value) > -1) return false; 591 | if (this.props.filterOption) return this.props.filterOption.call(this, op, filterValue); 592 | var valueTest = String(op.value), labelTest = String(op.label); 593 | if (this.props.ignoreCase) { 594 | valueTest = valueTest.toLowerCase(); 595 | labelTest = labelTest.toLowerCase(); 596 | filterValue = filterValue.toLowerCase(); 597 | } 598 | return !filterValue || (this.props.matchPos === 'start') ? ( 599 | (this.props.matchProp !== 'label' && valueTest.substr(0, filterValue.length) === filterValue) || 600 | (this.props.matchProp !== 'value' && labelTest.substr(0, filterValue.length) === filterValue) 601 | ) : ( 602 | (this.props.matchProp !== 'label' && valueTest.indexOf(filterValue) >= 0) || 603 | (this.props.matchProp !== 'value' && labelTest.indexOf(filterValue) >= 0) 604 | ); 605 | }; 606 | return (options || []).filter(filterOption, this); 607 | } 608 | }, 609 | 610 | selectFocusedOption: function() { 611 | if (this.props.allowCreate && !this.state.focusedOption) { 612 | return this.selectValue(this.state.inputValue); 613 | } 614 | 615 | if (this.state.focusedOption) { 616 | return this.selectValue(this.state.focusedOption); 617 | } 618 | }, 619 | 620 | focusOption: function(op) { 621 | this.setState({ 622 | focusedOption: op 623 | }); 624 | }, 625 | 626 | focusNextOption: function() { 627 | this.focusAdjacentOption('next'); 628 | }, 629 | 630 | focusPreviousOption: function() { 631 | this.focusAdjacentOption('previous'); 632 | }, 633 | 634 | focusAdjacentOption: function(dir) { 635 | this._focusedOptionReveal = true; 636 | var ops = this.state.filteredOptions.filter(function(op) { 637 | return !op.disabled; 638 | }); 639 | if (!this.state.isOpen) { 640 | this.setState({ 641 | isOpen: true, 642 | inputValue: '', 643 | focusedOption: this.state.focusedOption || ops[dir === 'next' ? 0 : ops.length - 1] 644 | }, this._bindCloseMenuIfClickedOutside); 645 | return; 646 | } 647 | if (!ops.length) { 648 | return; 649 | } 650 | var focusedIndex = -1; 651 | for (var i = 0; i < ops.length; i++) { 652 | if (this.state.focusedOption === ops[i]) { 653 | focusedIndex = i; 654 | break; 655 | } 656 | } 657 | var focusedOption = ops[0]; 658 | if (dir === 'next' && focusedIndex > -1 && focusedIndex < ops.length - 1) { 659 | focusedOption = ops[focusedIndex + 1]; 660 | } else if (dir === 'previous') { 661 | if (focusedIndex > 0) { 662 | focusedOption = ops[focusedIndex - 1]; 663 | } else { 664 | focusedOption = ops[ops.length - 1]; 665 | } 666 | } 667 | this.setState({ 668 | focusedOption: focusedOption 669 | }); 670 | }, 671 | 672 | unfocusOption: function(op) { 673 | if (this.state.focusedOption === op) { 674 | this.setState({ 675 | focusedOption: null 676 | }); 677 | } 678 | }, 679 | 680 | buildMenu: function() { 681 | var focusedValue = this.state.focusedOption ? this.state.focusedOption.value : null; 682 | var renderLabel = this.props.optionRenderer || function(op) { 683 | return op.label; 684 | }; 685 | if (this.state.filteredOptions.length > 0) { 686 | focusedValue = focusedValue == null ? this.state.filteredOptions[0] : focusedValue; 687 | } 688 | // Add the current value to the filtered options in last resort 689 | var options = this.state.filteredOptions; 690 | if (this.props.allowCreate && this.state.inputValue.trim()) { 691 | var inputValue = this.state.inputValue; 692 | options = options.slice(); 693 | var newOption = this.props.newOptionCreator ? this.props.newOptionCreator(inputValue) : { 694 | value: inputValue, 695 | label: inputValue, 696 | create: true 697 | }; 698 | options.unshift(newOption); 699 | } 700 | var ops = Object.keys(options).map(function(key) { 701 | var op = options[key]; 702 | var isSelected = this.state.value === op.value; 703 | var isFocused = focusedValue === op.value; 704 | var optionClass = classes({ 705 | 'Select-option': true, 706 | 'is-selected': isSelected, 707 | 'is-focused': isFocused, 708 | 'is-disabled': op.disabled 709 | }); 710 | var ref = isFocused ? 'focused' : null; 711 | var mouseEnter = this.focusOption.bind(this, op); 712 | var mouseLeave = this.unfocusOption.bind(this, op); 713 | var mouseDown = this.selectValue.bind(this, op); 714 | var optionResult = React.createElement(this.props.optionComponent, { 715 | key: 'option-' + op.value, 716 | className: optionClass, 717 | renderFunc: renderLabel, 718 | mouseEnter: mouseEnter, 719 | mouseLeave: mouseLeave, 720 | mouseDown: mouseDown, 721 | click: mouseDown, 722 | addLabelText: this.props.addLabelText, 723 | option: op, 724 | ref: ref 725 | }); 726 | return optionResult; 727 | }, this); 728 | 729 | if (ops.length) { 730 | return ops; 731 | } else { 732 | var noResultsText, promptClass; 733 | if (this.isLoading()) { 734 | promptClass = 'Select-searching'; 735 | noResultsText = this.props.searchingText; 736 | } else if (this.state.inputValue || !this.props.asyncOptions) { 737 | promptClass = 'Select-noresults'; 738 | noResultsText = this.props.noResultsText; 739 | } else { 740 | promptClass = 'Select-search-prompt'; 741 | noResultsText = this.props.searchPromptText; 742 | } 743 | 744 | return ( 745 |
746 | {noResultsText} 747 |
748 | ); 749 | } 750 | }, 751 | 752 | handleOptionLabelClick: function (value, event) { 753 | if (this.props.onOptionLabelClick) { 754 | this.props.onOptionLabelClick(value, event); 755 | } 756 | }, 757 | 758 | isLoading: function() { 759 | return this.props.isLoading || this.state.isLoading; 760 | }, 761 | 762 | render: function() { 763 | var selectClass = classes('Select', this.props.className, { 764 | 'is-multi': this.props.multi, 765 | 'is-searchable': this.props.searchable, 766 | 'is-open': this.state.isOpen, 767 | 'is-focused': this.state.isFocused, 768 | 'is-loading': this.isLoading(), 769 | 'is-disabled': this.props.disabled, 770 | 'has-value': this.state.value 771 | }); 772 | var value = []; 773 | if (this.props.multi) { 774 | this.state.values.forEach(function(val) { 775 | var onOptionLabelClick = this.handleOptionLabelClick.bind(this, val); 776 | var onRemove = this.removeValue.bind(this, val); 777 | var valueComponent = React.createElement(this.props.valueComponent, { 778 | key: val.value, 779 | option: val, 780 | renderer: this.props.valueRenderer, 781 | optionLabelClick: !!this.props.onOptionLabelClick, 782 | onOptionLabelClick: onOptionLabelClick, 783 | onRemove: onRemove, 784 | disabled: this.props.disabled 785 | }); 786 | value.push(valueComponent); 787 | }, this); 788 | } 789 | 790 | if (!this.state.inputValue && (!this.props.multi || !value.length)) { 791 | var val = this.state.values[0] || null; 792 | if (this.props.valueRenderer && !!this.state.values.length) { 793 | value.push(); 798 | } else { 799 | var singleValueComponent = React.createElement(this.props.singleValueComponent, { 800 | key: 'placeholder', 801 | value: val, 802 | placeholder: this.state.placeholder 803 | }); 804 | value.push(singleValueComponent); 805 | } 806 | } 807 | 808 | var loading = this.isLoading() ?