Skip to content

Commit

Permalink
Merge pull request #125 from yesmeck/automatic-tokenization
Browse files Browse the repository at this point in the history
Automatic tokenization
  • Loading branch information
yiminghe authored Nov 1, 2016
2 parents a599a85 + ff707ba commit e846c25
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 2 deletions.
1 change: 1 addition & 0 deletions examples/multiple.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const Test = React.createClass({
onDeselect={onDeselect}
placeholder="please select"
onChange={this.onChange}
tokenSeparators={[' ', ',']}
>
{children}
</Select>
Expand Down
1 change: 1 addition & 0 deletions examples/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const Test = React.createClass({
maxTagTextLength={10}
value={this.state.value}
onChange={this.onChange}
tokenSeparators={[' ', ',']}
>
{children}
</Select>
Expand Down
56 changes: 54 additions & 2 deletions src/Select.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
isSingleMode, toArray, findIndexInValueByKey,
UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE,
preventDefaultEvent, findFirstMenuItem,
includesSeparators, splitBySeparators,
findIndexInValueByLabel,
} from './util';
import SelectTrigger from './SelectTrigger';
import FilterMixin from './FilterMixin';
Expand Down Expand Up @@ -74,6 +76,7 @@ const Select = React.createClass({
]),
dropdownStyle: PropTypes.object,
maxTagTextLength: PropTypes.number,
tokenSeparators: PropTypes.arrayOf(PropTypes.string),
},

mixins: [FilterMixin],
Expand Down Expand Up @@ -170,13 +173,22 @@ const Select = React.createClass({
},

onInputChange(event) {
const { tokenSeparators } = this.props;
const val = event.target.value;
const { props } = this;
if (isMultipleOrTags(this.props) &&
tokenSeparators &&
includesSeparators(val, tokenSeparators)) {
const nextValue = this.tokenize(val);
this.fireChange(nextValue);
this.setOpenState(false, true);
this.setInputValue('', false);
return;
}
this.setInputValue(val);
this.setState({
open: true,
});
if (isCombobox(props)) {
if (isCombobox(this.props)) {
this.fireChange([{
key: val,
}]);
Expand Down Expand Up @@ -384,6 +396,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);
},
Expand Down Expand Up @@ -615,6 +645,28 @@ const Select = React.createClass({
});
},

tokenize(string) {
const { multiple, tokenSeparators, children } = this.props;
let nextValue = this.state.value;
splitBySeparators(string, tokenSeparators).forEach(label => {
const selectedValue = { key: label, label };
if (findIndexInValueByLabel(nextValue, label) === -1) {
if (multiple) {
const value = this.getValueByLabel(children, label);
if (value) {
selectedValue.key = value;
nextValue = nextValue.concat(selectedValue);
return;
}
} else {
nextValue = nextValue.concat(selectedValue);
return;
}
}
});
return nextValue;
},

renderTopControlNode() {
const { value, open, inputValue } = this.state;
const props = this.props;
Expand Down
32 changes: 32 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
Expand Down Expand Up @@ -103,3 +114,24 @@ export function findFirstMenuItem(children) {
}
return null;
}

export function includesSeparators(string, separators) {
for (let i = 0; i < separators.length; ++i) {
if (string.lastIndexOf(separators[i]) > 0) {
return true;
}
}
return false;
}

export function splitBySeparators(string, separators) {
const reg = new RegExp(`[${separators.join()}]`);
const array = string.split(reg);
if (array[0] === '') {
array.shift();
}
if (array[array.length - 1] === '') {
array.pop();
}
return array;
}
41 changes: 41 additions & 0 deletions tests/Select.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,45 @@ describe('Select', () => {
done();
});
});

describe('automatic tokenization ', () => {
it('tokenize tag select', () => {
instance = ReactDOM.render(
<Select tags tokenSeparators={[',']}>
<Option value="1">1</Option>
<Option value="2">2</Option>
</Select>,
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(
<Select multiple optionLabelProp="children" tokenSeparators={[',']}>
<Option value="1">One</Option>
<Option value="2">Two</Option>
</Select>,
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' },
]);
});
});
});
40 changes: 40 additions & 0 deletions tests/util.spec.js
Original file line number Diff line number Diff line change
@@ -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([]);
});
});

0 comments on commit e846c25

Please sign in to comment.