From 0461293a54f10765cf2dc1678cfc79d43d8f0ea8 Mon Sep 17 00:00:00 2001 From: Wei Zhu Date: Sun, 30 Oct 2016 02:01:17 +0800 Subject: [PATCH 1/5] Implement automatic tokenization --- examples/tags.js | 1 + src/Select.jsx | 18 ++++++++++++++++-- src/util.js | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/examples/tags.js b/examples/tags.js index a041eb03f..b588ab59d 100644 --- a/examples/tags.js +++ b/examples/tags.js @@ -43,6 +43,7 @@ const Test = React.createClass({ maxTagTextLength={10} value={this.state.value} onChange={this.onChange} + tokenSeparators={[' ', ',']} > {children} diff --git a/src/Select.jsx b/src/Select.jsx index 60c8c5cb6..8a5a9bd1a 100644 --- a/src/Select.jsx +++ b/src/Select.jsx @@ -11,6 +11,7 @@ import { isSingleMode, toArray, findIndexInValueByKey, UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE, preventDefaultEvent, findFirstMenuItem, + includesSeparators, splitBySeparators, } from './util'; import SelectTrigger from './SelectTrigger'; import FilterMixin from './FilterMixin'; @@ -73,6 +74,7 @@ const Select = React.createClass({ ]), dropdownStyle: PropTypes.object, maxTagTextLength: PropTypes.number, + tokenSeparators: PropTypes.arrayOf(PropTypes.string), }, mixins: [FilterMixin], @@ -168,13 +170,25 @@ const Select = React.createClass({ }, onInputChange(event) { + const { tags, tokenSeparators } = this.props; const val = event.target.value; - const { props } = this; + if (tags && tokenSeparators && includesSeparators(val, tokenSeparators)) { + let nextValue = this.state.value; + splitBySeparators(val, tokenSeparators).forEach(value => { + const selectedValue = { key: value, label: value }; + if (findIndexInValueByKey(nextValue, value) === -1) { + nextValue = nextValue.concat(selectedValue); + } + }); + this.fireChange(nextValue); + this.setOpenState(false, true); + return; + } this.setInputValue(val); this.setState({ open: true, }); - if (isCombobox(props)) { + if (isCombobox(this.props)) { this.fireChange([{ key: val, }]); diff --git a/src/util.js b/src/util.js index f34bfd223..010de6898 100644 --- a/src/util.js +++ b/src/util.js @@ -103,3 +103,20 @@ export function findFirstMenuItem(children) { } return null; } + +export function includesSeparators(string, separators) { + if (~separators.indexOf(string)) { + return false; + } + for (let i = 0; i < separators.length; ++i) { + if (~string.indexOf(separators[i])) { + return true; + } + } + return false; +} + +export function splitBySeparators(string, separators) { + const reg = new RegExp(`[${separators.join()}]`); + return string.split(reg); +} From ea219f083e487f563c948609bbff4932e2bcc724 Mon Sep 17 00:00:00 2001 From: Wei Zhu Date: Sun, 30 Oct 2016 15:40:13 +0800 Subject: [PATCH 2/5] Add some tests --- src/Select.jsx | 1 + src/util.js | 14 +++++++++----- tests/util.spec.js | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 tests/util.spec.js diff --git a/src/Select.jsx b/src/Select.jsx index 8a5a9bd1a..b056e18cc 100644 --- a/src/Select.jsx +++ b/src/Select.jsx @@ -182,6 +182,7 @@ const Select = React.createClass({ }); this.fireChange(nextValue); this.setOpenState(false, true); + this.setInputValue('', false); return; } this.setInputValue(val); diff --git a/src/util.js b/src/util.js index 010de6898..57352574e 100644 --- a/src/util.js +++ b/src/util.js @@ -105,11 +105,8 @@ export function findFirstMenuItem(children) { } export function includesSeparators(string, separators) { - if (~separators.indexOf(string)) { - return false; - } for (let i = 0; i < separators.length; ++i) { - if (~string.indexOf(separators[i])) { + if (string.lastIndexOf(separators[i]) > 0) { return true; } } @@ -118,5 +115,12 @@ export function includesSeparators(string, separators) { export function splitBySeparators(string, separators) { const reg = new RegExp(`[${separators.join()}]`); - return string.split(reg); + const array = string.split(reg); + if (array[0] === '') { + array.shift(); + } + if (array[array.length - 1] === '') { + array.pop(); + } + return array; } diff --git a/tests/util.spec.js b/tests/util.spec.js new file mode 100644 index 000000000..73aa47721 --- /dev/null +++ b/tests/util.spec.js @@ -0,0 +1,40 @@ +import expect from 'expect.js'; +import { includesSeparators, splitBySeparators } from '../src/util'; + +describe('includesSeparators', () => { + const separators = [' ', ',']; + it('return true when given includes separators', () => { + expect(includesSeparators(',foo,bar', separators)).to.be(true); + }); + + it('return false when given do not include separators', () => { + expect(includesSeparators('foobar', separators)).to.be(false); + }); + + it('return false when string only has a leading separator', () => { + expect(includesSeparators(',foobar', separators)).to.be(false); + }); +}); + +describe('splitBySeparators', () => { + const separators = [' ', ',']; + it('split given string by separators', () => { + const string = 'foo bar,baz'; + expect(splitBySeparators(string, separators)).to.eql(['foo', 'bar', 'baz']); + }); + + it('split string with leading separator ', () => { + const string = ',foo'; + expect(splitBySeparators(string, separators)).to.eql(['foo']); + }); + + it('split string with trailling separator', () => { + const string = 'foo,'; + expect(splitBySeparators(string, separators)).to.eql(['foo']); + }); + + it('split a separator', () => { + const string = ','; + expect(splitBySeparators(string, separators)).to.eql([]); + }); +}); From a3c60e66c21838d883359ef3bee4420e18b8d290 Mon Sep 17 00:00:00 2001 From: Wei Zhu Date: Sun, 30 Oct 2016 16:44:11 +0800 Subject: [PATCH 3/5] Support multiple select --- examples/multiple.js | 1 + src/Select.jsx | 22 ++++++++++++++++++---- src/util.js | 11 +++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/examples/multiple.js b/examples/multiple.js index 388de68f1..14326b2ec 100644 --- a/examples/multiple.js +++ b/examples/multiple.js @@ -66,6 +66,7 @@ const Test = React.createClass({ onDeselect={onDeselect} placeholder="please select" onChange={this.onChange} + tokenSeparators={[' ', ',']} > {children} diff --git a/src/Select.jsx b/src/Select.jsx index b056e18cc..1e8b4731c 100644 --- a/src/Select.jsx +++ b/src/Select.jsx @@ -12,6 +12,7 @@ import { UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE, preventDefaultEvent, findFirstMenuItem, includesSeparators, splitBySeparators, + findIndexInValueByLabel, } from './util'; import SelectTrigger from './SelectTrigger'; import FilterMixin from './FilterMixin'; @@ -170,14 +171,27 @@ const Select = React.createClass({ }, onInputChange(event) { - const { tags, tokenSeparators } = this.props; + const { multiple, tokenSeparators } = this.props; const val = event.target.value; - if (tags && tokenSeparators && includesSeparators(val, tokenSeparators)) { + if (isMultipleOrTags(this.props) && + tokenSeparators && + includesSeparators(val, tokenSeparators)) { let nextValue = this.state.value; splitBySeparators(val, tokenSeparators).forEach(value => { const selectedValue = { key: value, label: value }; - if (findIndexInValueByKey(nextValue, value) === -1) { - nextValue = nextValue.concat(selectedValue); + if (findIndexInValueByLabel(nextValue, value) === -1) { + if (multiple) { + for (let i = 0; i < this._options.length; i++) { + if (this.getLabelFromOption(this._options[i]).join('') === value) { + selectedValue.key = this._options[i].key; + nextValue = nextValue.concat(selectedValue); + return; + } + } + } else { + nextValue = nextValue.concat(selectedValue); + return; + } } }); this.fireChange(nextValue); diff --git a/src/util.js b/src/util.js index 57352574e..b5b240d61 100644 --- a/src/util.js +++ b/src/util.js @@ -60,6 +60,17 @@ export function findIndexInValueByKey(value, key) { return index; } +export function findIndexInValueByLabel(value, label) { + let index = -1; + for (let i = 0; i < value.length; i++) { + if (toArray(value[i].label).join('') === label) { + index = i; + break; + } + } + return index; +} + export function getSelectKeys(menuItems, value) { if (value === null || value === undefined) { return []; From 1a3dd7cbe7212165dba86b2e486f3146e5c818a0 Mon Sep 17 00:00:00 2001 From: Wei Zhu Date: Sun, 30 Oct 2016 16:50:54 +0800 Subject: [PATCH 4/5] A little refactor --- src/Select.jsx | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/Select.jsx b/src/Select.jsx index 1e8b4731c..ebfaa90c2 100644 --- a/src/Select.jsx +++ b/src/Select.jsx @@ -171,29 +171,12 @@ const Select = React.createClass({ }, onInputChange(event) { - const { multiple, tokenSeparators } = this.props; + const { tokenSeparators } = this.props; const val = event.target.value; if (isMultipleOrTags(this.props) && - tokenSeparators && - includesSeparators(val, tokenSeparators)) { - let nextValue = this.state.value; - splitBySeparators(val, tokenSeparators).forEach(value => { - const selectedValue = { key: value, label: value }; - if (findIndexInValueByLabel(nextValue, value) === -1) { - if (multiple) { - for (let i = 0; i < this._options.length; i++) { - if (this.getLabelFromOption(this._options[i]).join('') === value) { - selectedValue.key = this._options[i].key; - nextValue = nextValue.concat(selectedValue); - return; - } - } - } else { - nextValue = nextValue.concat(selectedValue); - return; - } - } - }); + tokenSeparators && + includesSeparators(val, tokenSeparators)) { + const nextValue = this.tokenize(val); this.fireChange(nextValue); this.setOpenState(false, true); this.setInputValue('', false); @@ -641,6 +624,29 @@ const Select = React.createClass({ }); }, + tokenize(string) { + const { multiple, tokenSeparators } = this.props; + let nextValue = this.state.value; + splitBySeparators(string, tokenSeparators).forEach(value => { + const selectedValue = { key: value, label: value }; + if (findIndexInValueByLabel(nextValue, value) === -1) { + if (multiple) { + for (let i = 0; i < this._options.length; i++) { + if (this.getLabelFromOption(this._options[i]).join('') === value) { + selectedValue.key = this._options[i].key; + nextValue = nextValue.concat(selectedValue); + return; + } + } + } else { + nextValue = nextValue.concat(selectedValue); + return; + } + } + }); + return nextValue; + }, + renderTopControlNode() { const { value, open, inputValue } = this.state; const props = this.props; From ff707ba19937cb3f544f526ffafecf7c876106ee Mon Sep 17 00:00:00 2001 From: Wei Zhu Date: Sun, 30 Oct 2016 17:54:01 +0800 Subject: [PATCH 5/5] Add tests for tokenize --- src/Select.jsx | 37 +++++++++++++++++++++++++++---------- tests/Select.spec.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/Select.jsx b/src/Select.jsx index ebfaa90c2..4f5c907f7 100644 --- a/src/Select.jsx +++ b/src/Select.jsx @@ -393,6 +393,24 @@ const Select = React.createClass({ return label; }, + getValueByLabel(children, label) { + if (label === undefined) { + return null; + } + let value = null; + React.Children.forEach(children, (child) => { + if (child.type === OptGroup) { + const maybe = this.getValueByLabel(child.props.children, label); + if (maybe !== null) { + value = maybe; + } + } else if (toArray(this.getLabelFromOption(child)).join('') === label) { + value = getValuePropValue(child); + } + }); + return value; + }, + getLabelFromOption(child) { return getPropValue(child, this.props.optionLabelProp); }, @@ -625,18 +643,17 @@ const Select = React.createClass({ }, tokenize(string) { - const { multiple, tokenSeparators } = this.props; + const { multiple, tokenSeparators, children } = this.props; let nextValue = this.state.value; - splitBySeparators(string, tokenSeparators).forEach(value => { - const selectedValue = { key: value, label: value }; - if (findIndexInValueByLabel(nextValue, value) === -1) { + splitBySeparators(string, tokenSeparators).forEach(label => { + const selectedValue = { key: label, label }; + if (findIndexInValueByLabel(nextValue, label) === -1) { if (multiple) { - for (let i = 0; i < this._options.length; i++) { - if (this.getLabelFromOption(this._options[i]).join('') === value) { - selectedValue.key = this._options[i].key; - nextValue = nextValue.concat(selectedValue); - return; - } + const value = this.getValueByLabel(children, label); + if (value) { + selectedValue.key = value; + nextValue = nextValue.concat(selectedValue); + return; } } else { nextValue = nextValue.concat(selectedValue); diff --git a/tests/Select.spec.js b/tests/Select.spec.js index aaa99ff46..97cfe48c9 100644 --- a/tests/Select.spec.js +++ b/tests/Select.spec.js @@ -176,4 +176,45 @@ describe('Select', () => { done(); }); }); + + describe('automatic tokenization ', () => { + it('tokenize tag select', () => { + instance = ReactDOM.render( + , + div); + const input = TestUtils.findRenderedDOMComponentWithTag(instance, 'input'); + + input.value = '2,3,4'; + Simulate.change(input); + + expect(instance.state.value).to.eql([ + { key: '2', label: '2' }, + { key: '3', label: '3' }, + { key: '4', label: '4' }, + ]); + }); + + it('tokenize multiple select', () => { + instance = ReactDOM.render( + , + div); + const input = TestUtils.findRenderedDOMComponentWithTag(instance, 'input'); + + input.value = 'One,'; + Simulate.change(input); + input.value = 'One,Two,Three'; + Simulate.change(input); + + expect(instance.state.value).to.eql([ + { key: '1', label: 'One' }, + { key: '2', label: 'Two' }, + ]); + }); + }); });