From bd2b0a99575c612bc4985210da703cccb7659612 Mon Sep 17 00:00:00 2001 From: Colby Rabideau Date: Thu, 24 Sep 2015 12:29:17 -0400 Subject: [PATCH 01/13] basic grouping --- examples/src/app.js | 2 + .../src/components/GroupedOptionsField.js | 46 +++++++ src/OptionGroup.js | 41 +++++++ src/Select.js | 113 ++++++++++++------ 4 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 examples/src/components/GroupedOptionsField.js create mode 100644 src/OptionGroup.js diff --git a/examples/src/app.js b/examples/src/app.js index cb06945ba6..79717ef55a 100644 --- a/examples/src/app.js +++ b/examples/src/app.js @@ -5,6 +5,7 @@ import Select from 'react-select'; import CustomRenderField from './components/CustomRenderField'; import MultiSelectField from './components/MultiSelectField'; +import GroupedOptionsField from './components/GroupedOptionsField'; import RemoteSelectField from './components/RemoteSelectField'; import SelectedValuesField from './components/SelectedValuesField'; import StatesField from './components/StatesField'; @@ -40,6 +41,7 @@ React.render( + , document.getElementById('example') ); diff --git a/examples/src/components/GroupedOptionsField.js b/examples/src/components/GroupedOptionsField.js new file mode 100644 index 0000000000..a0f71ca5a1 --- /dev/null +++ b/examples/src/components/GroupedOptionsField.js @@ -0,0 +1,46 @@ +import React from 'react'; +import Select from 'react-select'; + +function logChange() { + console.log.apply(console, [].concat(['Select value changed:'], Array.prototype.slice.apply(arguments))); +} + +var ops = [{ + label: 'Primary Colors', + options: [ + { label: 'Yellow', value: 'yellow' }, + { label: 'Red', value: 'red' }, + { label: 'Blue', value: 'blue' } + ] +}, { + label: 'Secondary Colors', + options: [ + { label: 'Orange', value: 'orange' }, + { label: 'Violet', value: 'violet' }, + { label: 'Green', value: 'green' } + ] +}]; + +var GroupedOptionsField = React.createClass({ + displayName: 'GroupedOptionsField', + propTypes: { + delimiter: React.PropTypes.string, + label: React.PropTypes.string, + multi: React.PropTypes.bool, + }, + + render () { + return ( +
+

{this.props.label}

+
); diff --git a/src/OptionGroup.js b/src/OptionGroup.js index 6d48d743ee..aa2c5af517 100644 --- a/src/OptionGroup.js +++ b/src/OptionGroup.js @@ -3,6 +3,7 @@ var classes = require('classnames'); var Option = React.createClass({ propTypes: { + children: React.PropTypes.any, className: React.PropTypes.string, optionGroup: React.PropTypes.shape({ label: React.PropTypes.string.isRequired, @@ -30,11 +31,15 @@ var Option = React.createClass({ var optionClasses = classes(this.props.className, obj.className); return ( -
- {renderedLabel} -
+
+
+ {renderedLabel} +
+ {this.props.children} +
); } }); diff --git a/src/Select.js b/src/Select.js index a4ec3c78c1..bd1b30f583 100644 --- a/src/Select.js +++ b/src/Select.js @@ -12,6 +12,48 @@ var OptionGroup = require('./OptionGroup'); var requestId = 0; +function defaultFilterOption(option, filterValue) { + if (!filterValue) { + return true; + } + var valueTest = String(option.value), labelTest = String(option.label); + if (this.props.ignoreCase) { + valueTest = valueTest.toLowerCase(); + labelTest = labelTest.toLowerCase(); + filterValue = filterValue.toLowerCase(); + } + return (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) + ); +} + +function defaultFilterOptions(options, filterValue, exclude) { + if (!options) { + return []; + } + var matchedOptions = function(matches, option) { + if (isGroup(option)) { + var groupMatches = option.options.reduce(matchedOptions, []); + if (groupMatches.length > 0) { + matches.push(Object.assign({}, option, { + options: groupMatches, + })); + } + } else if ( + (!this.props.multi || exclude.indexOf(option.value) === -1) && + (this.props.filterOption && this.props.filterOption.call(this, option, filterValue)) + ) { + matches.push(option); + } + return matches; + }.bind(this); + return options.reduce(matchedOptions, []); +} + function defaultLabelRenderer(op) { return op.label; } @@ -95,6 +137,8 @@ var Select = React.createClass({ clearable: true, delimiter: ',', disabled: false, + filterOption: defaultFilterOption, + filterOptions: defaultFilterOptions, ignoreCase: true, inputProps: {}, matchPos: 'any', @@ -132,7 +176,8 @@ var Select = React.createClass({ isFocused: false, isLoading: false, isOpen: false, - options: flattenOptions(this.props.options) + options: this.props.options, + flatOptions: flattenOptions(this.props.options) }; }, @@ -191,10 +236,11 @@ var Select = React.createClass({ var optionsChanged = false; if (JSON.stringify(newProps.options) !== JSON.stringify(this.props.options)) { optionsChanged = true; - var flatOptions = flattenOptions(newProps.options); + var options = newProps.options; this.setState({ - options: flatOptions, - filteredOptions: this.filterOptions(flatOptions) + flatOptions: flattenOptions(options), + filteredOptions: this.filterOptions(options), + options: options }); } if (newProps.value !== this.state.value || newProps.placeholder !== this.props.placeholder || optionsChanged) { @@ -607,29 +653,8 @@ var Select = React.createClass({ var exclude = (values || this.state.values).map(function(i) { return i.value; }); - if (this.props.filterOptions) { - return this.props.filterOptions.call(this, options, filterValue, exclude); - } else { - var filterOption = function(op) { - if (isGroup(op)) return op.options.some(filterOption, this); - if (this.props.multi && exclude.indexOf(op.value) > -1) return false; - if (this.props.filterOption) return this.props.filterOption.call(this, op, filterValue); - var valueTest = String(op.value), labelTest = String(op.label); - if (this.props.ignoreCase) { - valueTest = valueTest.toLowerCase(); - labelTest = labelTest.toLowerCase(); - filterValue = filterValue.toLowerCase(); - } - 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) - ); - }; - return (options || []).filter(filterOption, this); - } + var result = this.props.filterOptions.call(this, options, filterValue, exclude); + return result; }, selectFocusedOption: function() { @@ -658,7 +683,7 @@ var Select = React.createClass({ focusAdjacentOption: function(dir) { this._focusedOptionReveal = true; - var ops = this.state.filteredOptions.filter(function(op) { + var ops = this.state.flatOptions.filter(function(op) { return !op.disabled && !isGroup(op); }); if (!this.state.isOpen) { @@ -719,12 +744,13 @@ var Select = React.createClass({ }; options.unshift(newOption); } + var ops = Object.keys(options).map(function(key) { var op = options[key]; if (isGroup(op)) { - return this.renderOptionGroup(op); + return this.renderOptionGroup(focusedValue, op); } - return this.renderOption(op, focusedValue); + return this.renderOption(focusedValue, op); }, this); if (ops.length) { @@ -750,7 +776,7 @@ var Select = React.createClass({ } }, - renderOption(op, focusedValue) { + renderOption(focusedValue, op) { var renderLabel = this.props.optionRenderer || defaultLabelRenderer; var isSelected = this.state.value === op.value; var isFocused = focusedValue === op.value; @@ -778,12 +804,12 @@ var Select = React.createClass({ }); }, - renderOptionGroup(group) { + renderOptionGroup(focusedValue, group) { var renderLabel = this.props.optionGroupRenderer || defaultLabelRenderer; - var optionClass = classes('Select-option', 'is-disabled'); return React.createElement(this.props.optionGroupComponent, { key: 'optionGroup-' + group.label, - className: optionClass, + className: 'Select-option Select-optionGroup', + children: group.options.map(this.renderOption.bind(this, focusedValue)), renderFunc: renderLabel, optionGroup: group }); @@ -846,7 +872,7 @@ var Select = React.createClass({ var menu; var menuProps; - if (this.state.isOpen) { + if (this.props.open || this.state.isOpen) { menuProps = { ref: 'menu', className: 'Select-menu', From cd74be2192b32e823deaa750fa38cfda4a3ae662 Mon Sep 17 00:00:00 2001 From: Colby Rabideau Date: Thu, 24 Sep 2015 15:45:25 -0400 Subject: [PATCH 09/13] mousing across groups --- .../src/components/GroupedOptionsField.js | 40 ++++++--- src/OptionGroup.js | 6 +- src/Select.js | 84 +++++++++++-------- 3 files changed, 78 insertions(+), 52 deletions(-) diff --git a/examples/src/components/GroupedOptionsField.js b/examples/src/components/GroupedOptionsField.js index 164e47bbc0..b55f40babf 100644 --- a/examples/src/components/GroupedOptionsField.js +++ b/examples/src/components/GroupedOptionsField.js @@ -10,18 +10,37 @@ var ops = [{ value: 'black', }, { label: 'Primary Colors', - options: [ - { label: 'Yellow', value: 'yellow' }, - { label: 'Red', value: 'red' }, - { label: 'Blue', value: 'blue' } - ] + options: [{ + label: 'Yellow', + value: 'yellow' + }, { + label: 'Red', + value: 'red' + }, { + label: 'Blue', + value: 'blue' + }] }, { label: 'Secondary Colors', - options: [ - { label: 'Orange', value: 'orange' }, - { label: 'Violet', value: 'violet' }, - { label: 'Green', value: 'green' } - ] + options: [{ + label: 'Orange', + value: 'orange' + }, { + label: 'Purple', + options: [{ + label: 'Light Purple', + value: 'light_purple' + }, { + label: 'Medium Purple', + value: 'medium_purple' + }, { + label: 'Dark Purple', + value: 'dark_purple' + }] + }, { + label: 'Green', + value: 'green' + }] }, { label: 'White', value: 'white', @@ -40,7 +59,6 @@ var GroupedOptionsField = React.createClass({

{this.props.label}