├── 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 |
30 | {this.renderHint()}
31 |
32 | );
33 | }
34 | });
35 |
36 | module.exports = UsersField;
--------------------------------------------------------------------------------
/examples/src/components/CustomOption.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Gravatar from 'react-gravatar';
3 |
4 | var Option = React.createClass({
5 | propTypes: {
6 | addLabelText: React.PropTypes.string,
7 | className: React.PropTypes.string,
8 | mouseDown: React.PropTypes.func,
9 | mouseEnter: React.PropTypes.func,
10 | mouseLeave: React.PropTypes.func,
11 | option: React.PropTypes.object.isRequired,
12 | renderFunc: React.PropTypes.func
13 | },
14 | render () {
15 | var obj = this.props.option;
16 | var size = 15;
17 | var gravatarStyle = {
18 | borderRadius: 3,
19 | display: 'inline-block',
20 | marginRight: 10,
21 | position: 'relative',
22 | top: -2,
23 | verticalAlign: 'middle',
24 | };
25 | return (
26 |
31 |
32 | {obj.value}
33 |
34 | );
35 | }
36 | });
37 |
38 | module.exports = Option;
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Jed Watson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/src/components/SelectedValuesField.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 SelectedValuesField = React.createClass({
9 | displayName: 'SelectedValuesField',
10 | propTypes: {
11 | allowCreate: React.PropTypes.bool,
12 | hint: React.PropTypes.string,
13 | label: React.PropTypes.string,
14 | options: React.PropTypes.array,
15 | },
16 | onLabelClick (data, event) {
17 | console.log(data, event);
18 | },
19 | renderHint () {
20 | if (!this.props.hint) return null;
21 | return (
22 | {this.props.hint}
23 | );
24 | },
25 | render () {
26 | return (
27 |
28 |
{this.props.label}
29 |
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 |
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 |
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 |
37 |
38 | );
39 | }
40 | });
41 | module.exports = DisabledUpsellOptions;
42 |
--------------------------------------------------------------------------------
/examples/src/components/CustomRenderField.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 CustomRenderField = React.createClass({
9 | displayName: 'CustomRenderField',
10 | propTypes: {
11 | delimiter: React.PropTypes.string,
12 | label: React.PropTypes.string,
13 | multi: React.PropTypes.bool,
14 | },
15 | renderOption (option) {
16 | return {option.label} ({option.hex}) ;
17 |
18 | },
19 | renderValue (option) {
20 | return {option.label} ;
21 | },
22 | render () {
23 | var ops = [
24 | { label: 'Red', value: 'red', hex: '#EC6230' },
25 | { label: 'Green', value: 'green', hex: '#4ED84E' },
26 | { label: 'Blue', value: 'blue', hex: '#6D97E2' }
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 |
59 | {this.renderHint()}
60 |
61 | );
62 | }
63 | });
64 |
65 | module.exports = RemoteSelectField;
66 |
--------------------------------------------------------------------------------
/examples/src/components/MultiSelectField.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 MultiSelectField = React.createClass({
9 | displayName: 'MultiSelectField',
10 | propTypes: {
11 | label: React.PropTypes.string,
12 | },
13 | getInitialState () {
14 | return {
15 | disabled: false,
16 | value: []
17 | };
18 | },
19 | handleSelectChange (value, values) {
20 | logChange('New value:', value, 'Values:', values);
21 | this.setState({ value: value });
22 | },
23 | toggleDisabled (e) {
24 | this.setState({ 'disabled': e.target.checked });
25 | },
26 | render () {
27 | var ops = [
28 | { label: 'Chocolate', value: 'chocolate' },
29 | { label: 'Vanilla', value: 'vanilla' },
30 | { label: 'Strawberry', value: 'strawberry' },
31 | { label: 'Caramel', value: 'caramel' },
32 | { label: 'Cookies and Cream', value: 'cookiescream' },
33 | { label: 'Peppermint', value: 'peppermint' }
34 | ];
35 | return (
36 |
37 |
{this.props.label}
38 |
39 |
40 |
41 |
42 |
43 | Disabled
44 |
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 |
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 |
32 |
48 |
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 |
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 |
32 |
48 |
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 | [](https://travis-ci.org/JedWatson/react-select)
2 | [](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 |
78 | ```
79 |
80 | ### Multiselect options
81 |
82 | You can enable multi-value selection by setting `multi={true}`. In this mode:
83 |
84 | * Selected options will be removed from the dropdown menu
85 | * The values of the selected items are joined using the `delimiter` property to create the input value
86 | * A simple value, if provided, will be split using the `delimiter` property
87 | * The `onChange` event provides an array of the selected options as the second argument
88 | * The first argument to `onChange` is always a string, regardless of whether the values of the selected options are numbers or strings
89 | * By default, only options in the `options` array can be selected. Setting `allowCreate` to true allows new options to be created if they do not already exist.
90 |
91 | ### Async options
92 |
93 | If you want to load options asynchronously, instead of providing an `options` Array, provide a `asyncOptions` Function.
94 |
95 | The function takes two arguments `String input, Function callback`and will be called when the input text is changed.
96 |
97 | When your async process finishes getting the options, pass them to `callback(err, data)` in a Object `{ options: [] }`.
98 |
99 | The select control will intelligently cache options for input strings that have already been fetched. The cached result set will be filtered as more specific searches are input, so if your async process would only return a smaller set of results for a more specific query, also pass `complete: true` in the callback object. Caching can be disabled by setting `cacheAsyncResults` to `false` (Note that `complete: true` will then have no effect).
100 |
101 | Unless you specify the property `autoload={false}` the control will automatically load the default set of options (i.e. for `input: ''`) when it is mounted.
102 |
103 | ```
104 | var Select = require('react-select');
105 |
106 | var getOptions = function(input, callback) {
107 | setTimeout(function() {
108 | callback(null, {
109 | options: [
110 | { value: 'one', label: 'One' },
111 | { value: 'two', label: 'Two' }
112 | ],
113 | // CAREFUL! Only set this to true when there are no more options,
114 | // or more specific queries will not be sent to the server.
115 | complete: true
116 | });
117 | }, 500);
118 | };
119 |
120 |
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 |
141 | ```
142 |
143 | ### Filtering options
144 |
145 | You can control how options are filtered with the following properties:
146 |
147 | * `matchPos`: `"start"` or `"any"`: whether to match the text entered at the start or any position in the option value
148 | * `matchProp`: `"label"`, `"value"` or `"any"`: whether to match the value, label or both values of each option when filtering
149 | * `ignoreCase`: `Boolean`: whether to ignore case or match the text exactly when filtering
150 |
151 | `matchProp` and `matchPos` both default to `"any"`.
152 | `ignoreCase` defaults to `true`.
153 |
154 | #### Advanced filters
155 |
156 | You can also completely replace the method used to filter either a single option, or the entire options array (allowing custom sort mechanisms, etc.)
157 |
158 | * `filterOption`: `function(Object option, String filter)` returns `Boolean`. Will override `matchPos`, `matchProp` and `ignoreCase` options.
159 | * `filterOptions`: `function(Array options, String filter, Array currentValues)` returns `Array filteredOptions`. Will override `filterOption`, `matchPos`, `matchProp` and `ignoreCase` options.
160 |
161 | For multi-select inputs, when providing a custom `filterOptions` method, remember to exclude current values from the returned array of options.
162 |
163 | ### Further options
164 |
165 |
166 | Property | Type | Description
167 | :-----------------------|:--------------|:--------------------------------
168 | addLabelText | string | text to display when allowCreate is true
169 | allowCreate | bool | allow new options to be created in multi mode (displays an "Add \ ?" item when a value not already in the `options` array is entered)
170 | asyncOptions | func | function to call to get options
171 | autoload | bool | whether to auto-load the default async options set
172 | backspaceRemoves | bool | whether pressing backspace removes the last item when there is no input value
173 | cacheAsyncResults | bool | enables the options cache for asyncOptions (default: `true`)
174 | className | string | className for the outer element
175 | clearable | bool | should it be possible to reset value
176 | clearAllText | string | title for the "clear" control when multi: true
177 | clearValueText | string | title for the "clear" control
178 | delimiter | string | delimiter to use to join multiple values
179 | disabled | bool | whether the Select is disabled or not
180 | filterOption | func | method to filter a single option: function(option, filterString)
181 | filterOptions | func | method to filter the options array: function([options], filterString, [values])
182 | ignoreCase | bool | whether to perform case-insensitive filtering
183 | inputProps | object | custom attributes for the Input (in the Select-control) e.g: {'data-foo': 'bar'}
184 | isLoading | bool | whether the Select is loading externally or not (such as options being loaded)
185 | matchPos | string | (any, start) match the start or entire string when filtering
186 | matchProp | string | (any, label, value) which option property to filter on
187 | multi | bool | multi-value input
188 | name | string | field name, for hidden 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 `` elements should be considered private and prone to change.
208 |
209 | ```js
210 | // focuses the input element
211 | .focus();
212 | ```
213 |
214 | # Contributing
215 |
216 | See our [CONTRIBUTING.md](https://github.com/JedWatson/react-select/blob/master/CONTRIBUTING.md) for information on how to contribute.
217 |
218 |
219 | # License
220 |
221 | MIT Licensed. Copyright (c) Jed Watson 2015.
222 |
--------------------------------------------------------------------------------
/examples/dist/example.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: rgba(0, 126, 255, 0.08);
216 | border-radius: 2px;
217 | border: 1px solid rgba(0, 126, 255, 0.24);
218 | color: #007eff;
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: #007eff;
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 rgba(0, 126, 255, 0.24);
243 | padding: 2px 5px 4px;
244 | }
245 | .Select-item-icon:hover,
246 | .Select-item-icon:focus {
247 | background-color: rgba(0, 113, 230, 0.08);
248 | color: #0071e6;
249 | }
250 | .Select-item-icon:active {
251 | background-color: rgba(0, 126, 255, 0.24);
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 | body {
278 | color: #333333;
279 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
280 | font-size: 14px;
281 | line-height: 1.4;
282 | margin: 0;
283 | padding: 0;
284 | }
285 | a {
286 | color: #007eff;
287 | text-decoration: none;
288 | }
289 | a:hover {
290 | text-decoration: underline;
291 | }
292 | .container {
293 | margin-left: auto;
294 | margin-right: auto;
295 | max-width: 400px;
296 | padding: 0 30px;
297 | }
298 | h1,
299 | h2,
300 | h3,
301 | h4,
302 | h5,
303 | h6,
304 | .h1,
305 | .h2,
306 | .h3,
307 | .h4,
308 | .h5,
309 | .h6 {
310 | color: black;
311 | font-weight: 500;
312 | line-height: 1;
313 | margin-bottom: .66em;
314 | margin-top: 0;
315 | }
316 | h1,
317 | .h1 {
318 | font-size: 3em;
319 | }
320 | h2,
321 | .h2 {
322 | font-size: 2em;
323 | font-weight: 300;
324 | }
325 | h3,
326 | .h3 {
327 | font-size: 1.25em;
328 | }
329 | h4,
330 | .h4 {
331 | font-size: 1em;
332 | }
333 | h5,
334 | .h5 {
335 | font-size: .85em;
336 | }
337 | h6,
338 | .h6 {
339 | font-size: .75em;
340 | }
341 | .page-body,
342 | .page-footer,
343 | .page-header {
344 | padding: 30px 0;
345 | }
346 | .page-header {
347 | background-color: #007eff;
348 | color: #bfdfff;
349 | }
350 | .page-header h1,
351 | .page-header h2,
352 | .page-header h3 {
353 | color: white;
354 | }
355 | .page-header p {
356 | font-size: 1.2em;
357 | margin: 0;
358 | }
359 | .page-header a {
360 | border-bottom: 1px solid rgba(255, 255, 255, 0.3);
361 | color: white;
362 | text-decoration: none;
363 | }
364 | .page-header a:hover,
365 | .page-header a:focus {
366 | border-bottom-color: white;
367 | outline: none;
368 | text-decoration: none;
369 | }
370 | .page-subheader {
371 | background-color: #e6f2ff;
372 | line-height: 20px;
373 | padding: 30px 0;
374 | }
375 | .page-subheader__button {
376 | float: right;
377 | }
378 | .page-subheader__link {
379 | border-bottom: 1px solid rgba(0, 126, 255, 0.3);
380 | outline: none;
381 | text-decoration: none;
382 | }
383 | .page-subheader__link:hover,
384 | .page-subheader__link:focus {
385 | border-bottom-color: #007eff;
386 | outline: none;
387 | text-decoration: none;
388 | }
389 | .page-footer {
390 | background-color: #fafafa;
391 | color: #999;
392 | padding: 30px 0;
393 | text-align: center;
394 | }
395 | @media (min-width: 480px) {
396 | .page-body,
397 | .page-header {
398 | padding: 60px 0;
399 | }
400 | .page-header {
401 | font-size: 1.4em;
402 | }
403 | .page-subheader {
404 | font-size: 1.125em;
405 | line-height: 28px;
406 | }
407 | }
408 | .checkbox-list {
409 | margin-top: .5em;
410 | overflow: hidden;
411 | }
412 | .checkbox-list > .checkbox {
413 | clear: left;
414 | float: left;
415 | margin-top: .5em;
416 | }
417 | .checkbox-control {
418 | margin-right: .5em;
419 | position: relative;
420 | top: -1px;
421 | }
422 | .switcher {
423 | color: #999;
424 | cursor: default;
425 | font-size: 12px;
426 | margin: 10px 0;
427 | text-transform: uppercase;
428 | }
429 | .switcher .link {
430 | color: #007eff;
431 | cursor: pointer;
432 | font-weight: bold;
433 | margin-left: 10px;
434 | }
435 | .switcher .link:hover {
436 | text-decoration: underline;
437 | }
438 | .switcher .active {
439 | color: #666;
440 | font-weight: bold;
441 | margin-left: 10px;
442 | }
443 | .section {
444 | margin-bottom: 40px;
445 | }
446 | .hint {
447 | font-size: .85em;
448 | margin: 15px 0;
449 | color: #666;
450 | }
451 | /*
452 |
453 | // include these styles to test normal form fields
454 |
455 | .form-input {
456 | margin-bottom: 15px;
457 | }
458 |
459 | .form-input input {
460 | background-color: white;
461 | border-color: lighten(@select-input-border-color, 5%) @select-input-border-color darken(@select-input-border-color, 10%);
462 | border-radius: @select-input-border-radius;
463 | border: 1px solid @select-input-border-color;
464 | box-sizing: border-box;
465 | color: @select-text-color;
466 | font-size: 14px;
467 | outline: none;
468 | padding: @select-padding-vertical @select-padding-horizontal;
469 | transition: all 200ms ease;
470 | width: 100%;
471 |
472 | &:hover {
473 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
474 | }
475 |
476 | &:focus {
477 | border-color: @select-input-border-focus lighten(@select-input-border-focus, 5%) lighten(@select-input-border-focus, 5%);
478 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px fade(@select-input-border-focus,50%);
479 | }
480 | }
481 |
482 | */
483 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # React-Select
2 |
3 | ## v0.6.11 / 2015-09-28
4 |
5 | * added; `isLoading` prop, allows indication of async options loading in situations where more control is required, thanks [Jon Gautsch](https://github.com/jgautsch)
6 |
7 | ## v0.6.10 / 2015-09-24
8 |
9 | * fixed; a build issue with the previous release that prevented the stylesheet being generated / included
10 | * fixed; a LESS syntax issue, thanks [Bob Cardenas](https://github.com/bcardi)
11 |
12 | ## v0.6.9 / 2015-09-19
13 |
14 | * added; `style` key for package.json, thanks [Stephen Wan](https://github.com/stephen)
15 | * added; `onInputChange` handler that returns the current input value, thanks [Tom Leslie](https://github.com/lomteslie)
16 | * fixed; simplifying handleKey function & preventDefault behaviour, thanks [davidpene](https://github.com/davidpene)
17 | * fixed; Display spinner while auto-loading initial data, thanks [Ben Jenkinson](https://github.com/BenJenkinson)
18 | * fixed; better support for touch events, thanks [Montlouis-Calixte Stéphane](https://github.com/bulby97)
19 | * fixed; prevent value splitting on non-multi-value select, thanks [Alan R. Soares](https://github.com/alanrsoares)
20 |
21 | ## v0.6.8 / 2015-09-16
22 |
23 | * fixed; broader range of allowed prereleases for React 0.14, including rc1
24 | * fixed; preventing backspace from navigating back in the browser history, thanks [davidpene](https://github.com/davidpene)
25 |
26 | ## v0.6.7 / 2015-08-28
27 |
28 | * fixed; missing styles for `.Select-search-prompt` and `.Select-searching` issues, thanks [Jaak Erisalu](https://github.com/jaakerisalu) and [davidpene](https://github.com/davidpene)
29 |
30 | ## v0.6.6 / 2015-08-26
31 |
32 | * fixed; issue in Chrome where clicking the scrollbar would close the menu, thanks [Vladimir Matsola](https://github.com/vomchik)
33 |
34 | ## v0.6.5 / 2015-08-24
35 |
36 | * fixed; completely ignores clicks on disabled items, unless the target of the click is a link, thanks [Ben Stahl](https://github.com/bhstahl)
37 |
38 | ## v0.6.4 / 2015-08-24
39 |
40 | This release includes a huge improvement to the examples / website thanks to @jossmac. Also:
41 |
42 | * added; support for React 0.14 beta3
43 | * fixed; disabled options after searching, thanks @bruderstein
44 | * added; support for "Searching..." text (w/ prop) while loading async results, thanks @bruderstein and @johnomalley
45 | * added; `className`, `style` and `title` keys are now supported in option properties, thanks @bruderstein
46 |
47 | ## v0.6.3 / 2015-08-18
48 |
49 | Otherwise known as "the real 0.6.2" this includes the updated build for the last version; sorry about that!
50 |
51 | ## v0.6.2 / 2015-08-13
52 |
53 | * changed; if the `searchable` prop is `false`, the menu is opened _or closed_ on click, more like a standard Select input. thanks [MaaikeB](https://github.com/MaaikeB)
54 |
55 | ## v0.6.1 / 2015-08-09
56 |
57 | * added; Support for options with numeric values, thanks [Dave Brotherstone](https://github.com/bruderstein)
58 | * changed; Disabled options now appear in the search results
, thanks [Dave Brotherstone](https://github.com/bruderstein)
59 | * fixed; asyncOptions are reloaded on componentWillReceiveProps when the value has changed, thanks [Francis Cote](https://github.com/drfeelgoud)
60 | * added; `cacheAsyncResults` prop (default `true`) now controls whether the internal cache is used for `asyncOptions`
61 |
62 | ## v0.6.0 / 2015-08-05
63 |
64 | * improved; option, value and single value have been split out into their own components, and can be customised with props. see [#328](https://github.com/JedWatson/react-select/pull/328) for more details.
65 | * improved; Near-complete test coverage thanks to the awesome work of [Dave Brotherstone](https://github.com/bruderstein)
66 | * improved; Support all alpha/beta/rc's of React 0.14.0, thanks [Sébastien Lorber](https://github.com/slorber)
67 | * fixed; Close multi-select menu when tabbing away, thanks [Ben Alpert](https://github.com/spicyj)
68 | * fixed; Bug where Select shows the value instead of the label (reapplying fix)
69 | * fixed; `valueRenderer` now works when `multi={false}`, thanks [Chris Portela](https://github.com/0xCMP)
70 | * added; New property `backspaceRemoves` (default `true`), allows the default behaviour of removing values with backspace when `multi={true}`, thanks [Leo Lehikoinen](https://github.com/lehikol2)
71 |
72 | ## v0.5.6 / 2015-07-27
73 |
74 | * fixed; Allow entering of commas when allowCreate is on but multi is off, thanks [Angelo DiNardi](https://github.com/adinardi)
75 | * fixed; Times (clear) character is now rendered from string unicode character for consistent output, thanks [Nibbles](https://github.com/Siliconrob)
76 | * fixed; allowCreate bug, thanks [goodzsq](https://github.com/goodzsq)
77 | * fixed; changes to props.placeholder weren't being reflected correctly, thanks [alesn](https://github.com/alesn)
78 | * fixed; error when escape is pressedn where `clearValue` was not passed the event, thanks [Mikhail Kotelnikov](https://github.com/mkotelnikov)
79 | * added; More tests, thanks [Dave Brotherstone](https://github.com/bruderstein)
80 |
81 | ## v0.5.5 / 2015-07-12
82 |
83 | * fixed; replaced usage of `component.getDOMNode()` with `React.findDOMNode(component)` for compatibility with React 0.14
84 |
85 | ## v0.5.4 / 2015-07-06
86 |
87 | * fixed; regression in 0.5.3 that broke componentWillMount, sorry everyone!
88 | * added; `addLabelText` prop for customising the "add {label}?" text when in tags mode, thanks [Fenn](https://github.com/Fenntasy)
89 |
90 | ## v0.5.3 / 2015-07-05
91 |
92 | * fixed; autoload issues, thanks [Maxime Tyler](https://github.com/iam4x)
93 | * fixed; style incompatibilities with Foundation framework, thanks [Timothy Kempf](https://github.com/Fauntleroy)
94 |
95 | ## v0.5.2 / 2015-06-28
96 |
97 | * fixed; bug where Select shows the value instead of the label, thanks [Stephen Demjanenko](https://github.com/sdemjanenko)
98 | * added; 'is-selected' classname is added to the selected option, thanks [Alexey Volodkin](https://github.com/miraks)
99 | * fixed; async options are now loaded with the initial value, thanks [Pokai Chang](https://github.com/Neson)
100 | * fixed; `react-input-autosize` now correctly escapes ampersands (&), not actually a fix in react-select but worth noting here because it would have been causing a problem in `react-select` as well.
101 |
102 | ## v0.5.1 / 2015-06-21
103 |
104 | * added; custom option and value rendering capability, thanks [Brian Reavis](https://github.com/brianreavis)
105 | * fixed; collapsing issue when single-select or empty multi-select fields are disabled
106 | * fixed; issue where an empty value would be left after clearing all values in a multi-select field
107 |
108 | ## v0.5.0 / 2015-06-20
109 |
110 | * fixed; `esc` key incorrectly created empty options, thanks [rgrzelak](https://github.com/rgrzelak)
111 | * adeed; New feature to allow option creation ("tags mode"), enable with `allowCreate` prop, thanks [Florent Vilmart](https://github.com/flovilmart) and [Brian Reavis](https://github.com/brianreavis)
112 | * fixed; IE8 compatibility fallback for `addEventListener/removeEventListener`, which don't exist in IE8, thanks [Stefan Billiet](https://github.com/StefanBilliet)
113 | * fixed; Undefined values when using asyncOptions, thanks [bannaN](https://github.com/bannaN)
114 | * fixed; Prevent add the last focused value when the drop down menu is closed / Pushing enter without dropdown open adds a value, thanks [Giuseppe](https://github.com/giuse88)
115 | * fixed; Callback context is undefined, thanks [Giuseppe](https://github.com/giuse88)
116 | * fixed; Issue with event being swallowed on Enter `keydown`, thanks [Kevin Burke](https://github.com/kembuco)
117 | * added; Support for case-insensitive filtering when `matchPos="start"`, thanks [wesrage](https://github.com/wesrage)
118 | * added; Support for customizable background color, thanks [John Morales](https://github.com/JohnMorales)
119 | * fixed; Updated ESLint and cleared up warnings, thanks [Alexander Shemetovsky](https://github.com/AlexKVal)
120 | * fixed; Close dropdown when clicking on select, thanks [Nik Butenko](https://github.com/nkbt)
121 | * added; Tests, and mocha test framework, thanks [Craig Dallimore](https://github.com/craigdallimore)
122 | * fixed; You can now start the example server and watch for changes with `npm start`
123 |
124 |
125 | ## v0.4.9 / 2015-05-11
126 |
127 | * fixed; focus was being grabbed by the select when `autoload` and `asyncOptions` were set
128 | * added; `focus` method on the component
129 | * added; support for disabled options, thanks [Pasha Palangpour](https://github.com/pashap)
130 | * improved; more closures, less binds, for better performance, thanks [Daniel Cousens](https://github.com/dcousens)
131 |
132 | ## v0.4.8 / 2015-05-02
133 |
134 | * fixed; restored `dist/default.css`
135 | * fixed; standalone example works again
136 | * fixed; clarified dependency documentation and added dependencies for Bower
137 | * fixed; Scoping issues in `_bindCloseMenuIfClickedOutside`, thanks [bannaN](https://github.com/bannaN)
138 | * fixed; Doesnt try to set focus afterupdate if component is disabled, thanks [bannaN](https://github.com/bannaN)
139 |
140 | ## v0.4.7 / 2015-04-21
141 |
142 | * improved; lodash is no longer a dependency, thanks [Daniel Lo Nigro](https://github.com/Daniel15)
143 |
144 | ## v0.4.6 / 2015-04-06
145 |
146 | * updated; dependencies, build process and input-autosize component
147 |
148 | ## v0.4.5 / 2015-03-28
149 |
150 | * fixed; issue with long options overlapping arrow and clear icons, thanks [Rohit Kalkur](https://github.com/rovolution)
151 |
152 | ## v0.4.4 / 2015-03-26
153 |
154 | * fixed; error handling click events when the menu is closed, thanks [Ilya Petrov](https://github.com/muromec)
155 | * fixed; issue where options will not be filtered in certain conditions, thanks [G. Kay Lee](https://github.com/gsklee)
156 |
157 | ## v0.4.3 / 2015-03-25
158 |
159 | * added tests and updated dependencies
160 |
161 | ## v0.4.2 / 2015-03-23
162 |
163 | * added; ESLint and contributing guide
164 | * fixed; incorrect `classnames` variable assignement in window scope
165 | * fixed; all ESLint errors and warnings (except invalid JSX undefined/unused vars due to ESLint bug)
166 | * fixed; first option is now focused correctly, thanks [Eivind Siqveland Larsen](https://github.com/esiqveland)
167 |
168 | ## v0.4.1 / 2015-03-20
169 |
170 | * fixed; IE11 issue: clicking on scrollbar within menu no longer closes menu, thanks [Rohit Kalkur](https://github.com/rovolution)
171 |
172 | ## v0.4.0 / 2015-03-12
173 |
174 | * updated; compatible with React 0.13
175 |
176 | ## v0.3.5 / 2015-03-09
177 |
178 | * improved; less/no repaint on scroll for preformance wins, thanks [jsmunich](https://github.com/jsmunich)
179 | * added; `onBlur` and `onFocus` event handlers, thanks [Jonas Budelmann](https://github.com/cloudkite)
180 | * added; support for `inputProps` prop, passed to the ` ` 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() ? : null;
809 | var clear = this.props.clearable && this.state.value && !this.props.disabled ? : null;
810 |
811 | var menu;
812 | var menuProps;
813 | if (this.state.isOpen) {
814 | menuProps = {
815 | ref: 'menu',
816 | className: 'Select-menu',
817 | onMouseDown: this.handleMouseDown
818 | };
819 | menu = (
820 |
821 |
{this.buildMenu()}
822 |
823 | );
824 | }
825 |
826 | var input;
827 | var inputProps = {
828 | ref: 'input',
829 | className: 'Select-input ' + (this.props.inputProps.className || ''),
830 | tabIndex: this.props.tabIndex || 0,
831 | onFocus: this.handleInputFocus,
832 | onBlur: this.handleInputBlur
833 | };
834 | for (var key in this.props.inputProps) {
835 | if (this.props.inputProps.hasOwnProperty(key) && key !== 'className') {
836 | inputProps[key] = this.props.inputProps[key];
837 | }
838 | }
839 |
840 | if (!this.props.disabled) {
841 | if (this.props.searchable) {
842 | input = ;
843 | } else {
844 | input =
;
845 | }
846 | } else if (!this.props.multi || !this.state.values.length) {
847 | input =
;
848 | }
849 |
850 | return (
851 |
852 |
853 |
854 | {value}
855 | {input}
856 |
857 |
858 | {loading}
859 | {clear}
860 |
861 | {menu}
862 |
863 | );
864 | }
865 |
866 | });
867 |
868 | module.exports = Select;
869 |
--------------------------------------------------------------------------------
/lib/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 | 'use strict';
6 |
7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8 |
9 | var React = require('react');
10 | var Input = require('react-input-autosize');
11 | var classes = require('classnames');
12 | var Value = require('./Value');
13 | var SingleValue = require('./SingleValue');
14 | var Option = require('./Option');
15 |
16 | var requestId = 0;
17 |
18 | var Select = React.createClass({
19 |
20 | displayName: 'Select',
21 |
22 | propTypes: {
23 | addLabelText: React.PropTypes.string, // placeholder displayed when you want to add a label on a multi-value input
24 | allowCreate: React.PropTypes.bool, // whether to allow creation of new entries
25 | asyncOptions: React.PropTypes.func, // function to call to get options
26 | autoload: React.PropTypes.bool, // whether to auto-load the default async options set
27 | backspaceRemoves: React.PropTypes.bool, // whether backspace removes an item if there is no text input
28 | cacheAsyncResults: React.PropTypes.bool, // whether to allow cache
29 | className: React.PropTypes.string, // className for the outer element
30 | clearAllText: React.PropTypes.string, // title for the "clear" control when multi: true
31 | clearValueText: React.PropTypes.string, // title for the "clear" control
32 | clearable: React.PropTypes.bool, // should it be possible to reset value
33 | delimiter: React.PropTypes.string, // delimiter to use to join multiple values
34 | disabled: React.PropTypes.bool, // whether the Select is disabled or not
35 | filterOption: React.PropTypes.func, // method to filter a single option: function(option, filterString)
36 | filterOptions: React.PropTypes.func, // method to filter the options array: function([options], filterString, [values])
37 | ignoreCase: React.PropTypes.bool, // whether to perform case-insensitive filtering
38 | inputProps: React.PropTypes.object, // custom attributes for the Input (in the Select-control) e.g: {'data-foo': 'bar'}
39 | isLoading: React.PropTypes.bool, // whether the Select is loading externally or not (such as options being loaded)
40 | matchPos: React.PropTypes.string, // (any|start) match the start or entire string when filtering
41 | matchProp: React.PropTypes.string, // (any|label|value) which option property to filter on
42 | multi: React.PropTypes.bool, // multi-value input
43 | name: React.PropTypes.string, // field name, for hidden tag
44 | newOptionCreator: React.PropTypes.func, // factory to create new options when allowCreate set
45 | noResultsText: React.PropTypes.string, // placeholder displayed when there are no matching search results
46 | onBlur: React.PropTypes.func, // onBlur handler: function(event) {}
47 | onChange: React.PropTypes.func, // onChange handler: function(newValue) {}
48 | onFocus: React.PropTypes.func, // onFocus handler: function(event) {}
49 | onInputChange: React.PropTypes.func, // onInputChange handler: function(inputValue) {}
50 | onOptionLabelClick: React.PropTypes.func, // onCLick handler for value labels: function (value, event) {}
51 | optionComponent: React.PropTypes.func, // option component to render in dropdown
52 | optionRenderer: React.PropTypes.func, // optionRenderer: function(option) {}
53 | options: React.PropTypes.array, // array of options
54 | placeholder: React.PropTypes.string, // field placeholder, displayed when there's no value
55 | searchable: React.PropTypes.bool, // whether to enable searching feature or not
56 | searchingText: React.PropTypes.string, // message to display whilst options are loading via asyncOptions
57 | searchPromptText: React.PropTypes.string, // label to prompt for search input
58 | singleValueComponent: React.PropTypes.func, // single value component when multiple is set to false
59 | value: React.PropTypes.any, // initial field value
60 | valueComponent: React.PropTypes.func, // value component to render in multiple mode
61 | valueRenderer: React.PropTypes.func // valueRenderer: function(option) {}
62 | },
63 |
64 | getDefaultProps: function getDefaultProps() {
65 | return {
66 | addLabelText: 'Add "{label}"?',
67 | allowCreate: false,
68 | asyncOptions: undefined,
69 | autoload: true,
70 | backspaceRemoves: true,
71 | cacheAsyncResults: true,
72 | className: undefined,
73 | clearAllText: 'Clear all',
74 | clearValueText: 'Clear value',
75 | clearable: true,
76 | delimiter: ',',
77 | disabled: false,
78 | ignoreCase: true,
79 | inputProps: {},
80 | isLoading: false,
81 | matchPos: 'any',
82 | matchProp: 'any',
83 | name: undefined,
84 | newOptionCreator: undefined,
85 | noResultsText: 'No results found',
86 | onChange: undefined,
87 | onInputChange: undefined,
88 | onOptionLabelClick: undefined,
89 | optionComponent: Option,
90 | options: undefined,
91 | placeholder: 'Select...',
92 | searchable: true,
93 | searchingText: 'Searching...',
94 | searchPromptText: 'Type to search',
95 | singleValueComponent: SingleValue,
96 | value: undefined,
97 | valueComponent: Value
98 | };
99 | },
100 |
101 | getInitialState: function getInitialState() {
102 | return {
103 | /*
104 | * set by getStateFromValue on componentWillMount:
105 | * - value
106 | * - values
107 | * - filteredOptions
108 | * - inputValue
109 | * - placeholder
110 | * - focusedOption
111 | */
112 | isFocused: false,
113 | isLoading: false,
114 | isOpen: false,
115 | options: this.props.options
116 | };
117 | },
118 |
119 | componentWillMount: function componentWillMount() {
120 | var _this = this;
121 |
122 | this._optionsCache = {};
123 | this._optionsFilterString = '';
124 | this._closeMenuIfClickedOutside = function (event) {
125 | if (!_this.state.isOpen) {
126 | return;
127 | }
128 | var menuElem = React.findDOMNode(_this.refs.selectMenuContainer);
129 | var controlElem = React.findDOMNode(_this.refs.control);
130 |
131 | var eventOccuredOutsideMenu = _this.clickedOutsideElement(menuElem, event);
132 | var eventOccuredOutsideControl = _this.clickedOutsideElement(controlElem, event);
133 |
134 | // Hide dropdown menu if click occurred outside of menu
135 | if (eventOccuredOutsideMenu && eventOccuredOutsideControl) {
136 | _this.setState({
137 | isOpen: false
138 | }, _this._unbindCloseMenuIfClickedOutside);
139 | }
140 | };
141 | this._bindCloseMenuIfClickedOutside = function () {
142 | if (!document.addEventListener && document.attachEvent) {
143 | document.attachEvent('onclick', this._closeMenuIfClickedOutside);
144 | } else {
145 | document.addEventListener('click', this._closeMenuIfClickedOutside);
146 | }
147 | };
148 | this._unbindCloseMenuIfClickedOutside = function () {
149 | if (!document.removeEventListener && document.detachEvent) {
150 | document.detachEvent('onclick', this._closeMenuIfClickedOutside);
151 | } else {
152 | document.removeEventListener('click', this._closeMenuIfClickedOutside);
153 | }
154 | };
155 | this.setState(this.getStateFromValue(this.props.value));
156 | },
157 |
158 | componentDidMount: function componentDidMount() {
159 | if (this.props.asyncOptions && this.props.autoload) {
160 | this.autoloadAsyncOptions();
161 | }
162 | },
163 |
164 | componentWillUnmount: function componentWillUnmount() {
165 | clearTimeout(this._blurTimeout);
166 | clearTimeout(this._focusTimeout);
167 | if (this.state.isOpen) {
168 | this._unbindCloseMenuIfClickedOutside();
169 | }
170 | },
171 |
172 | componentWillReceiveProps: function componentWillReceiveProps(newProps) {
173 | var _this2 = this;
174 |
175 | var optionsChanged = false;
176 | if (JSON.stringify(newProps.options) !== JSON.stringify(this.props.options)) {
177 | optionsChanged = true;
178 | this.setState({
179 | options: newProps.options,
180 | filteredOptions: this.filterOptions(newProps.options)
181 | });
182 | }
183 | if (newProps.value !== this.state.value || newProps.placeholder !== this.props.placeholder || optionsChanged) {
184 | var setState = function setState(newState) {
185 | _this2.setState(_this2.getStateFromValue(newProps.value, newState && newState.options || newProps.options, newProps.placeholder));
186 | };
187 | if (this.props.asyncOptions) {
188 | this.loadAsyncOptions(newProps.value, {}, setState);
189 | } else {
190 | setState();
191 | }
192 | }
193 | },
194 |
195 | componentDidUpdate: function componentDidUpdate() {
196 | var _this3 = this;
197 |
198 | if (!this.props.disabled && this._focusAfterUpdate) {
199 | clearTimeout(this._blurTimeout);
200 | this._focusTimeout = setTimeout(function () {
201 | _this3.getInputNode().focus();
202 | _this3._focusAfterUpdate = false;
203 | }, 50);
204 | }
205 | if (this._focusedOptionReveal) {
206 | if (this.refs.focused && this.refs.menu) {
207 | var focusedDOM = React.findDOMNode(this.refs.focused);
208 | var menuDOM = React.findDOMNode(this.refs.menu);
209 | var focusedRect = focusedDOM.getBoundingClientRect();
210 | var menuRect = menuDOM.getBoundingClientRect();
211 |
212 | if (focusedRect.bottom > menuRect.bottom || focusedRect.top < menuRect.top) {
213 | menuDOM.scrollTop = focusedDOM.offsetTop + focusedDOM.clientHeight - menuDOM.offsetHeight;
214 | }
215 | }
216 | this._focusedOptionReveal = false;
217 | }
218 | },
219 |
220 | focus: function focus() {
221 | this.getInputNode().focus();
222 | },
223 |
224 | clickedOutsideElement: function clickedOutsideElement(element, event) {
225 | var eventTarget = event.target ? event.target : event.srcElement;
226 | while (eventTarget != null) {
227 | if (eventTarget === element) return false;
228 | eventTarget = eventTarget.offsetParent;
229 | }
230 | return true;
231 | },
232 |
233 | getStateFromValue: function getStateFromValue(value, options, placeholder) {
234 | if (!options) {
235 | options = this.state.options;
236 | }
237 | if (!placeholder) {
238 | placeholder = this.props.placeholder;
239 | }
240 |
241 | // reset internal filter string
242 | this._optionsFilterString = '';
243 |
244 | var values = this.initValuesArray(value, options);
245 | var filteredOptions = this.filterOptions(options, values);
246 |
247 | var focusedOption;
248 | var valueForState = null;
249 | if (!this.props.multi && values.length) {
250 | focusedOption = values[0];
251 | valueForState = values[0].value;
252 | } else {
253 | focusedOption = this.getFirstFocusableOption(filteredOptions);
254 | valueForState = values.map(function (v) {
255 | return v.value;
256 | }).join(this.props.delimiter);
257 | }
258 |
259 | return {
260 | value: valueForState,
261 | values: values,
262 | inputValue: '',
263 | filteredOptions: filteredOptions,
264 | placeholder: !this.props.multi && values.length ? values[0].label : placeholder,
265 | focusedOption: focusedOption
266 | };
267 | },
268 |
269 | getFirstFocusableOption: function getFirstFocusableOption(options) {
270 |
271 | for (var optionIndex = 0; optionIndex < options.length; ++optionIndex) {
272 | if (!options[optionIndex].disabled) {
273 | return options[optionIndex];
274 | }
275 | }
276 | },
277 |
278 | initValuesArray: function initValuesArray(values, options) {
279 | if (!Array.isArray(values)) {
280 | if (typeof values === 'string') {
281 | values = values === '' ? [] : this.props.multi ? values.split(this.props.delimiter) : [values];
282 | } else {
283 | values = values !== undefined && values !== null ? [values] : [];
284 | }
285 | }
286 | return values.map(function (val) {
287 | if (typeof val === 'string' || typeof val === 'number') {
288 | for (var key in options) {
289 | if (options.hasOwnProperty(key) && options[key] && (options[key].value === val || typeof options[key].value === 'number' && options[key].value.toString() === val)) {
290 | return options[key];
291 | }
292 | }
293 | return { value: val, label: val };
294 | } else {
295 | return val;
296 | }
297 | });
298 | },
299 |
300 | setValue: function setValue(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 selectValue(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 addValue(value) {
320 | this.setValue(this.state.values.concat(value));
321 | },
322 |
323 | popValue: function popValue() {
324 | this.setValue(this.state.values.slice(0, this.state.values.length - 1));
325 | },
326 |
327 | removeValue: function removeValue(valueToRemove) {
328 | this.setValue(this.state.values.filter(function (value) {
329 | return value !== valueToRemove;
330 | }));
331 | },
332 |
333 | clearValue: function clearValue(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 resetValue() {
345 | this.setValue(this.state.value === '' ? null : this.state.value);
346 | },
347 |
348 | getInputNode: function getInputNode() {
349 | var input = this.refs.input;
350 | return this.props.searchable ? input : React.findDOMNode(input);
351 | },
352 |
353 | fireChangeEvent: function fireChangeEvent(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 handleMouseDown(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 handleMouseDownOnArrow(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 handleInputFocus(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 | } else {
412 | this._unbindCloseMenuIfClickedOutside();
413 | }
414 | });
415 | this._openAfterFocus = false;
416 | if (this.props.onFocus) {
417 | this.props.onFocus(event);
418 | }
419 | },
420 |
421 | handleInputBlur: function handleInputBlur(event) {
422 | var _this4 = this;
423 |
424 | this._blurTimeout = setTimeout(function () {
425 | if (_this4._focusAfterUpdate) return;
426 | _this4.setState({
427 | isFocused: false,
428 | isOpen: false
429 | });
430 | }, 50);
431 | if (this.props.onBlur) {
432 | this.props.onBlur(event);
433 | }
434 | },
435 |
436 | handleKeyDown: function handleKeyDown(event) {
437 | if (this.props.disabled) return;
438 | switch (event.keyCode) {
439 | case 8:
440 | // backspace
441 | if (!this.state.inputValue && this.props.backspaceRemoves) {
442 | event.preventDefault();
443 | this.popValue();
444 | }
445 | return;
446 | case 9:
447 | // tab
448 | if (event.shiftKey || !this.state.isOpen || !this.state.focusedOption) {
449 | return;
450 | }
451 | this.selectFocusedOption();
452 | break;
453 | case 13:
454 | // enter
455 | if (!this.state.isOpen) return;
456 |
457 | this.selectFocusedOption();
458 | break;
459 | case 27:
460 | // escape
461 | if (this.state.isOpen) {
462 | this.resetValue();
463 | } else if (this.props.clearable) {
464 | this.clearValue(event);
465 | }
466 | break;
467 | case 38:
468 | // up
469 | this.focusPreviousOption();
470 | break;
471 | case 40:
472 | // down
473 | this.focusNextOption();
474 | break;
475 | case 188:
476 | // ,
477 | if (this.props.allowCreate && this.props.multi) {
478 | event.preventDefault();
479 | event.stopPropagation();
480 | this.selectFocusedOption();
481 | } else {
482 | return;
483 | }
484 | break;
485 | default:
486 | return;
487 | }
488 | event.preventDefault();
489 | },
490 |
491 | // Ensures that the currently focused option is available in filteredOptions.
492 | // If not, returns the first available option.
493 | _getNewFocusedOption: function _getNewFocusedOption(filteredOptions) {
494 | for (var key in filteredOptions) {
495 | if (filteredOptions.hasOwnProperty(key) && filteredOptions[key] === this.state.focusedOption) {
496 | return filteredOptions[key];
497 | }
498 | }
499 | return this.getFirstFocusableOption(filteredOptions);
500 | },
501 |
502 | handleInputChange: function handleInputChange(event) {
503 | // assign an internal variable because we need to use
504 | // the latest value before setState() has completed.
505 | this._optionsFilterString = event.target.value;
506 |
507 | if (this.props.onInputChange) {
508 | this.props.onInputChange(event.target.value);
509 | }
510 |
511 | if (this.props.asyncOptions) {
512 | this.setState({
513 | isLoading: true,
514 | inputValue: event.target.value
515 | });
516 | this.loadAsyncOptions(event.target.value, {
517 | isLoading: false,
518 | isOpen: true
519 | }, this._bindCloseMenuIfClickedOutside);
520 | } else {
521 | var filteredOptions = this.filterOptions(this.state.options);
522 | this.setState({
523 | isOpen: true,
524 | inputValue: event.target.value,
525 | filteredOptions: filteredOptions,
526 | focusedOption: this._getNewFocusedOption(filteredOptions)
527 | }, this._bindCloseMenuIfClickedOutside);
528 | }
529 | },
530 |
531 | autoloadAsyncOptions: function autoloadAsyncOptions() {
532 | var _this5 = this;
533 |
534 | this.setState({
535 | isLoading: true
536 | });
537 | this.loadAsyncOptions(this.props.value || '', { isLoading: false }, function () {
538 | // update with fetched but don't focus
539 | _this5.setValue(_this5.props.value, false);
540 | });
541 | },
542 |
543 | loadAsyncOptions: function loadAsyncOptions(input, state, callback) {
544 | var _this6 = this;
545 |
546 | var thisRequestId = this._currentRequestId = requestId++;
547 | if (this.props.cacheAsyncResults) {
548 | for (var i = 0; i <= input.length; i++) {
549 | var cacheKey = input.slice(0, i);
550 | if (this._optionsCache[cacheKey] && (input === cacheKey || this._optionsCache[cacheKey].complete)) {
551 | var options = this._optionsCache[cacheKey].options;
552 | var filteredOptions = this.filterOptions(options);
553 | var newState = {
554 | options: options,
555 | filteredOptions: filteredOptions,
556 | focusedOption: this._getNewFocusedOption(filteredOptions)
557 | };
558 | for (var key in state) {
559 | if (state.hasOwnProperty(key)) {
560 | newState[key] = state[key];
561 | }
562 | }
563 | this.setState(newState);
564 | if (callback) callback.call(this, newState);
565 | return;
566 | }
567 | }
568 | }
569 |
570 | this.props.asyncOptions(input, function (err, data) {
571 | if (err) throw err;
572 | if (_this6.props.cacheAsyncResults) {
573 | _this6._optionsCache[input] = data;
574 | }
575 | if (thisRequestId !== _this6._currentRequestId) {
576 | return;
577 | }
578 | var filteredOptions = _this6.filterOptions(data.options);
579 | var newState = {
580 | options: data.options,
581 | filteredOptions: filteredOptions,
582 | focusedOption: _this6._getNewFocusedOption(filteredOptions)
583 | };
584 | for (var key in state) {
585 | if (state.hasOwnProperty(key)) {
586 | newState[key] = state[key];
587 | }
588 | }
589 | _this6.setState(newState);
590 | if (callback) callback.call(_this6, newState);
591 | });
592 | },
593 |
594 | filterOptions: function filterOptions(options, values) {
595 | var filterValue = this._optionsFilterString;
596 | var exclude = (values || this.state.values).map(function (i) {
597 | return i.value;
598 | });
599 | if (this.props.filterOptions) {
600 | return this.props.filterOptions.call(this, options, filterValue, exclude);
601 | } else {
602 | var filterOption = function filterOption(op) {
603 | if (this.props.multi && exclude.indexOf(op.value) > -1) return false;
604 | if (this.props.filterOption) return this.props.filterOption.call(this, op, filterValue);
605 | var valueTest = String(op.value),
606 | labelTest = String(op.label);
607 | if (this.props.ignoreCase) {
608 | valueTest = valueTest.toLowerCase();
609 | labelTest = labelTest.toLowerCase();
610 | filterValue = filterValue.toLowerCase();
611 | }
612 | return !filterValue || this.props.matchPos === 'start' ? this.props.matchProp !== 'label' && valueTest.substr(0, filterValue.length) === filterValue || this.props.matchProp !== 'value' && labelTest.substr(0, filterValue.length) === filterValue : this.props.matchProp !== 'label' && valueTest.indexOf(filterValue) >= 0 || this.props.matchProp !== 'value' && labelTest.indexOf(filterValue) >= 0;
613 | };
614 | return (options || []).filter(filterOption, this);
615 | }
616 | },
617 |
618 | selectFocusedOption: function selectFocusedOption() {
619 | if (this.props.allowCreate && !this.state.focusedOption) {
620 | return this.selectValue(this.state.inputValue);
621 | }
622 |
623 | if (this.state.focusedOption) {
624 | return this.selectValue(this.state.focusedOption);
625 | }
626 | },
627 |
628 | focusOption: function focusOption(op) {
629 | this.setState({
630 | focusedOption: op
631 | });
632 | },
633 |
634 | focusNextOption: function focusNextOption() {
635 | this.focusAdjacentOption('next');
636 | },
637 |
638 | focusPreviousOption: function focusPreviousOption() {
639 | this.focusAdjacentOption('previous');
640 | },
641 |
642 | focusAdjacentOption: function focusAdjacentOption(dir) {
643 | this._focusedOptionReveal = true;
644 | var ops = this.state.filteredOptions.filter(function (op) {
645 | return !op.disabled;
646 | });
647 | if (!this.state.isOpen) {
648 | this.setState({
649 | isOpen: true,
650 | inputValue: '',
651 | focusedOption: this.state.focusedOption || ops[dir === 'next' ? 0 : ops.length - 1]
652 | }, this._bindCloseMenuIfClickedOutside);
653 | return;
654 | }
655 | if (!ops.length) {
656 | return;
657 | }
658 | var focusedIndex = -1;
659 | for (var i = 0; i < ops.length; i++) {
660 | if (this.state.focusedOption === ops[i]) {
661 | focusedIndex = i;
662 | break;
663 | }
664 | }
665 | var focusedOption = ops[0];
666 | if (dir === 'next' && focusedIndex > -1 && focusedIndex < ops.length - 1) {
667 | focusedOption = ops[focusedIndex + 1];
668 | } else if (dir === 'previous') {
669 | if (focusedIndex > 0) {
670 | focusedOption = ops[focusedIndex - 1];
671 | } else {
672 | focusedOption = ops[ops.length - 1];
673 | }
674 | }
675 | this.setState({
676 | focusedOption: focusedOption
677 | });
678 | },
679 |
680 | unfocusOption: function unfocusOption(op) {
681 | if (this.state.focusedOption === op) {
682 | this.setState({
683 | focusedOption: null
684 | });
685 | }
686 | },
687 |
688 | buildMenu: function buildMenu() {
689 | var focusedValue = this.state.focusedOption ? this.state.focusedOption.value : null;
690 | var renderLabel = this.props.optionRenderer || function (op) {
691 | return op.label;
692 | };
693 | if (this.state.filteredOptions.length > 0) {
694 | focusedValue = focusedValue == null ? this.state.filteredOptions[0] : focusedValue;
695 | }
696 | // Add the current value to the filtered options in last resort
697 | var options = this.state.filteredOptions;
698 | if (this.props.allowCreate && this.state.inputValue.trim()) {
699 | var inputValue = this.state.inputValue;
700 | options = options.slice();
701 | var newOption = this.props.newOptionCreator ? this.props.newOptionCreator(inputValue) : {
702 | value: inputValue,
703 | label: inputValue,
704 | create: true
705 | };
706 | options.unshift(newOption);
707 | }
708 | var ops = Object.keys(options).map(function (key) {
709 | var op = options[key];
710 | var isSelected = this.state.value === op.value;
711 | var isFocused = focusedValue === op.value;
712 | var optionClass = classes({
713 | 'Select-option': true,
714 | 'is-selected': isSelected,
715 | 'is-focused': isFocused,
716 | 'is-disabled': op.disabled
717 | });
718 | var ref = isFocused ? 'focused' : null;
719 | var mouseEnter = this.focusOption.bind(this, op);
720 | var mouseLeave = this.unfocusOption.bind(this, op);
721 | var mouseDown = this.selectValue.bind(this, op);
722 | var optionResult = React.createElement(this.props.optionComponent, {
723 | key: 'option-' + op.value,
724 | className: optionClass,
725 | renderFunc: renderLabel,
726 | mouseEnter: mouseEnter,
727 | mouseLeave: mouseLeave,
728 | mouseDown: mouseDown,
729 | click: mouseDown,
730 | addLabelText: this.props.addLabelText,
731 | option: op,
732 | ref: ref
733 | });
734 | return optionResult;
735 | }, this);
736 |
737 | if (ops.length) {
738 | return ops;
739 | } else {
740 | var noResultsText, promptClass;
741 | if (this.isLoading()) {
742 | promptClass = 'Select-searching';
743 | noResultsText = this.props.searchingText;
744 | } else if (this.state.inputValue || !this.props.asyncOptions) {
745 | promptClass = 'Select-noresults';
746 | noResultsText = this.props.noResultsText;
747 | } else {
748 | promptClass = 'Select-search-prompt';
749 | noResultsText = this.props.searchPromptText;
750 | }
751 |
752 | return React.createElement(
753 | 'div',
754 | { className: promptClass },
755 | noResultsText
756 | );
757 | }
758 | },
759 |
760 | handleOptionLabelClick: function handleOptionLabelClick(value, event) {
761 | if (this.props.onOptionLabelClick) {
762 | this.props.onOptionLabelClick(value, event);
763 | }
764 | },
765 |
766 | isLoading: function isLoading() {
767 | return this.props.isLoading || this.state.isLoading;
768 | },
769 |
770 | render: function render() {
771 | var selectClass = classes('Select', this.props.className, {
772 | 'is-multi': this.props.multi,
773 | 'is-searchable': this.props.searchable,
774 | 'is-open': this.state.isOpen,
775 | 'is-focused': this.state.isFocused,
776 | 'is-loading': this.isLoading(),
777 | 'is-disabled': this.props.disabled,
778 | 'has-value': this.state.value
779 | });
780 | var value = [];
781 | if (this.props.multi) {
782 | this.state.values.forEach(function (val) {
783 | var onOptionLabelClick = this.handleOptionLabelClick.bind(this, val);
784 | var onRemove = this.removeValue.bind(this, val);
785 | var valueComponent = React.createElement(this.props.valueComponent, {
786 | key: val.value,
787 | option: val,
788 | renderer: this.props.valueRenderer,
789 | optionLabelClick: !!this.props.onOptionLabelClick,
790 | onOptionLabelClick: onOptionLabelClick,
791 | onRemove: onRemove,
792 | disabled: this.props.disabled
793 | });
794 | value.push(valueComponent);
795 | }, this);
796 | }
797 |
798 | if (!this.state.inputValue && (!this.props.multi || !value.length)) {
799 | var val = this.state.values[0] || null;
800 | if (this.props.valueRenderer && !!this.state.values.length) {
801 | value.push(React.createElement(Value, {
802 | key: 0,
803 | option: val,
804 | renderer: this.props.valueRenderer,
805 | disabled: this.props.disabled }));
806 | } else {
807 | var singleValueComponent = React.createElement(this.props.singleValueComponent, {
808 | key: 'placeholder',
809 | value: val,
810 | placeholder: this.state.placeholder
811 | });
812 | value.push(singleValueComponent);
813 | }
814 | }
815 |
816 | var loading = this.isLoading() ? React.createElement('span', { className: 'Select-loading', 'aria-hidden': 'true' }) : null;
817 | var clear = this.props.clearable && this.state.value && !this.props.disabled ? React.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;
818 |
819 | var menu;
820 | var menuProps;
821 | if (this.state.isOpen) {
822 | menuProps = {
823 | ref: 'menu',
824 | className: 'Select-menu',
825 | onMouseDown: this.handleMouseDown
826 | };
827 | menu = React.createElement(
828 | 'div',
829 | { ref: 'selectMenuContainer', className: 'Select-menu-outer' },
830 | React.createElement(
831 | 'div',
832 | menuProps,
833 | this.buildMenu()
834 | )
835 | );
836 | }
837 |
838 | var input;
839 | var inputProps = {
840 | ref: 'input',
841 | className: 'Select-input ' + (this.props.inputProps.className || ''),
842 | tabIndex: this.props.tabIndex || 0,
843 | onFocus: this.handleInputFocus,
844 | onBlur: this.handleInputBlur
845 | };
846 | for (var key in this.props.inputProps) {
847 | if (this.props.inputProps.hasOwnProperty(key) && key !== 'className') {
848 | inputProps[key] = this.props.inputProps[key];
849 | }
850 | }
851 |
852 | if (!this.props.disabled) {
853 | if (this.props.searchable) {
854 | input = React.createElement(Input, _extends({ value: this.state.inputValue, onChange: this.handleInputChange, minWidth: '5' }, inputProps));
855 | } else {
856 | input = React.createElement(
857 | 'div',
858 | inputProps,
859 | ' '
860 | );
861 | }
862 | } else if (!this.props.multi || !this.state.values.length) {
863 | input = React.createElement(
864 | 'div',
865 | { className: 'Select-input' },
866 | ' '
867 | );
868 | }
869 |
870 | return React.createElement(
871 | 'div',
872 | { ref: 'wrapper', className: selectClass },
873 | React.createElement('input', { type: 'hidden', ref: 'value', name: this.props.name, value: this.state.value, disabled: this.props.disabled }),
874 | React.createElement(
875 | 'div',
876 | { className: 'Select-control', ref: 'control', onKeyDown: this.handleKeyDown, onMouseDown: this.handleMouseDown, onTouchEnd: this.handleMouseDown },
877 | value,
878 | input,
879 | React.createElement('span', { className: 'Select-arrow-zone', onMouseDown: this.handleMouseDownOnArrow }),
880 | React.createElement('span', { className: 'Select-arrow', onMouseDown: this.handleMouseDownOnArrow }),
881 | loading,
882 | clear
883 | ),
884 | menu
885 | );
886 | }
887 |
888 | });
889 |
890 | module.exports = Select;
--------------------------------------------------------------------------------