diff --git a/README.md b/README.md
index 2860b92..1c5b839 100644
--- a/README.md
+++ b/README.md
@@ -73,7 +73,7 @@ React.render(, document.getElementById('app'))
- [`autofocus`](#autofocus-optional)
- [`autoresize`](#autoresize-optional)
- [`delimiters`](#delimiters-optional)
-- [`delimiterChars`](#delimitersChars-optional)
+- [`delimiterChars`](#delimiterChars-optional)
- [`minQueryLength`](#minquerylength-optional)
- [`maxSuggestionsLength`](#maxsuggestionslength-optional)
- [`classNames`](#classnames-optional)
@@ -121,11 +121,11 @@ Boolean parameter to control whether the text-input should be automatically resi
#### delimiters (optional)
-Array of integers matching keyboard event `keyCode` values. When a corresponding key is pressed, the preceding string is finalised as tag. Default: `[9, 13]` (Tab and return keys).
+Array of integers matching keyboard event `keyCode` values. When a corresponding key is pressed, the preceding string is finalised as tag. Best used for non-printable keys, such as the tab and enter/return keys. Default: `[9, 13]` (Tab and return keys).
#### delimiterChars (optional)
-Array of characters matching keyboard event `key` values. This is useful when needing to support a specific character irrespective of the keyboard layout. Note, that this list is separate from the one specified by the `delimiters` option, so you'll need to set the value there to `[]`, if you wish to disable those keys. Example usage: `delimiterChars={[',', ' ']}`. Default: `[]`
+Array of characters matching characters that can be displayed in an input field. This is useful when needing to support a specific character irrespective of the keyboard layout, such as for internationalisation. Example usage: `delimiterChars={[',', ' ']}`. Default: `[',']`
#### minQueryLength (optional)
@@ -156,7 +156,7 @@ Override the default class names. Defaults:
#### handleAddition (required)
-Function called when the user wants to add a tag. Receives the tag.
+Function called when the user wants to add one or more tags. Receives the tag or tags. Value can be a tag or an Array of tags.
```js
function (tag) {
diff --git a/example/main.js b/example/main.js
index 1170719..50a98ab 100644
--- a/example/main.js
+++ b/example/main.js
@@ -33,6 +33,7 @@ class App extends React.Component {
return (
0) {
+ const regex = new RegExp('[' + this.escapeForRegExp(delimiterChars.join('')) + ']')
+
+ let tagsToAdd = []
+
+ // only process if query contains a delimiter character
+ if (query.match(regex)) {
+ // split the string based on the delimiterChars as a regex, being sure
+ // to escape chars, to prevent them being treated as special characters
+ // also remove any pure white-space entries
+ const tags = query.split(regex).filter((tag) => {
+ return tag.trim().length !== 0
+ })
+
+ // handle the case where the last character was not a delimiter, to
+ // avoid matching text a user was not ready to lookup
+ let maxTagIdx = tags.length
+ if (delimiterChars.indexOf(query.charAt(query.length - 1)) < 0) {
+ --maxTagIdx
+ }
+
+ // deal with case where we don't allow new tags, for now just stop processing
+ if (!this.props.allowNew) {
+ let lastTag = tags[maxTagIdx - 1]
+
+ const match = this.props.suggestions.findIndex((suggestion) => {
+ return suggestion.name.toLowerCase() === lastTag.trim().toLowerCase()
+ })
+
+ if (match < 0) {
+ let toOffset = query.length - 1
+ // deal with difference between typing and pasting
+ if (delimiterChars.indexOf(query.charAt(toOffset)) < 0) {
+ toOffset++
+ }
+ this.setState({ query: query.substring(0, toOffset) })
+ return
+ }
+ }
+
+ for (let i = 0; i < maxTagIdx; i++) {
+ // the logic here is similar to handleKeyDown, but subtly different,
+ // due to the context of the operation
+ const tag = tags[i].trim()
+ if (tag.length > 0) {
+ // look to see if the tag is already known, ignoring case
+ const matchIdx = this.props.suggestions.findIndex((suggestion) => {
+ return tag.toLowerCase() === suggestion.name.toLowerCase()
+ })
+
+ // if already known add it, otherwise add it only if we allow new tags
+ /* istanbul ignore else: sanity check */
+ if (matchIdx > -1) {
+ tagsToAdd.push(this.props.suggestions[matchIdx])
+ } else if (this.props.allowNew) {
+ tagsToAdd.push({ name: tag.trim() })
+ }
+ }
+ }
+
+ // Add all the found tags. We do it one shot, to avoid potential
+ // state issues.
+ this.addTag(tagsToAdd)
+
+ // if there was remaining undelimited text, add it to the query
+ if (maxTagIdx < tags.length) {
+ this.setState({ query: tags[maxTagIdx].trim() })
+ }
+ }
+ }
}
+ /**
+ * Handles the keydown event. This method allows handling of special keys,
+ * such as tab, enter and other meta keys. Use the `delimiter` property
+ * to define these keys.
+ *
+ * Note, While the `KeyboardEvent.keyCode` is considered deprecated, a limitation
+ * in Android Chrome, related to soft keyboards, prevents us from using the
+ * `KeyboardEvent.key` attribute. Any other scenario, not handled by this method,
+ * and related to printable characters, is handled by the `handleChange()` method.
+ */
handleKeyDown (e) {
const { query, selectedIndex } = this.state
- const { delimiters, delimiterChars } = this.props
+ const { delimiters } = this.props
// when one of the terminating keys is pressed, add current query to the tags.
- if (delimiters.indexOf(e.keyCode) > -1 || delimiterChars.indexOf(e.key) > -1) {
+ if (delimiters.indexOf(e.keyCode) > -1) {
if (query || selectedIndex > -1) {
e.preventDefault()
}
@@ -119,12 +222,22 @@ class ReactTags extends React.Component {
this.setState({ focused: true })
}
- addTag (tag) {
- if (tag.disabled) {
+ addTag (tags) {
+ let filteredTags = tags
+
+ if (!Array.isArray(filteredTags)) {
+ filteredTags = [filteredTags]
+ }
+
+ filteredTags = filteredTags.filter((tag) => {
+ return tag.disabled !== true
+ })
+
+ if (filteredTags.length === 0) {
return
}
- this.props.handleAddition(tag)
+ this.props.handleAddition(filteredTags)
// reset the state
this.setState({
@@ -194,7 +307,7 @@ ReactTags.defaultProps = {
autofocus: true,
autoresize: true,
delimiters: [KEYS.TAB, KEYS.ENTER],
- delimiterChars: [],
+ delimiterChars: [','],
minQueryLength: 2,
maxSuggestionsLength: 6,
allowNew: false,
diff --git a/package.json b/package.json
index 13fda52..bf317b7 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
"prop-types": "^15.5.0",
"react": "^15.5.0",
"react-dom": "^15.5.0",
- "sinon": "^1.17.5",
+ "sinon": "^4.0.0",
"standard": "^7.1.2",
"webpack": "^1.9.4",
"webpack-dev-server": "^1.8.2"
diff --git a/spec/ReactTags.spec.js b/spec/ReactTags.spec.js
index 947443e..f50074f 100644
--- a/spec/ReactTags.spec.js
+++ b/spec/ReactTags.spec.js
@@ -52,6 +52,12 @@ function type (value) {
})
}
+function paste (value) {
+ $('input').value = value
+ // React calls onchange following paste
+ TestUtils.Simulate.change($('input'))
+}
+
function key () {
Array.from(arguments).forEach((value) => {
TestUtils.Simulate.keyDown($('input'), { value, keyCode: keycode(value), key: value })
@@ -156,7 +162,7 @@ describe('React Tags', () => {
type(query); key('enter')
sinon.assert.calledOnce(props.handleAddition)
- sinon.assert.calledWith(props.handleAddition, { name: query })
+ sinon.assert.calledWith(props.handleAddition, [{ name: query }])
})
it('can add new tags when a delimiter character is entered', () => {
@@ -166,6 +172,76 @@ describe('React Tags', () => {
sinon.assert.calledThrice(props.handleAddition)
})
+
+ it('decriments maxTagIdx, when final character is not a separator', () => {
+ createInstance({ delimiterChars: [','], allowNew: true })
+
+ const input = $('input')
+
+ paste('antarctica, spain')
+
+ sinon.assert.calledOnce(props.handleAddition)
+ sinon.assert.calledWith(props.handleAddition, [{ name: 'antarctica' }])
+
+ expect(input.value).toEqual('spain')
+ })
+
+ it('adds value on paste, where values are delimiter terminated', () => {
+ createInstance({ delimiterChars: [','], allowNew: true, handleAddition: props.handleAddition })
+
+ paste('Algeria,Guinea Bissau,')
+
+ sinon.assert.calledOnce(props.handleAddition)
+ sinon.assert.calledWith(props.handleAddition, [{ name: 'Algeria' }, { name: 'Guinea Bissau' }])
+ })
+
+ it('does not process final tag on paste, if unrecognised tag', () => {
+ createInstance({ delimiterChars: [','], allowNew: false, suggestions: fixture })
+
+ paste('Thailand,Indonesia')
+
+ expect($('input').value).toEqual('Indonesia')
+ })
+
+ it('does not process final tag on paste, if unrecognised tag (white-space test)', () => {
+ createInstance({ delimiterChars: [','], allowNew: false, suggestions: fixture })
+
+ paste('Thailand, Algeria, Indonesia')
+
+ expect($('input').value).toEqual('Indonesia')
+ })
+
+ it('does not process final text on paste, if final text is not delimiter terminated', () => {
+ createInstance({ delimiterChars: [','], allowNew: false, suggestions: fixture })
+
+ paste('Thailand,Algeria')
+
+ expect($('input').value).toEqual('Algeria')
+ })
+
+ it('checks the trailing delimiter is removed on paste, when tag unrecognised', () => {
+ createInstance({ delimiterChars: [','], allowNew: false, suggestions: fixture })
+
+ paste('United Arab Emirates, Mars,')
+
+ expect($('input').value).toEqual('United Arab Emirates, Mars')
+ })
+
+ it('checks the trailing delimiter is removed on typing, when tag unrecognised', () => {
+ createInstance({ delimiterChars: [','], allowNew: false, suggestions: fixture })
+
+ type('Mars,')
+
+ expect($('input').value).toEqual('Mars')
+ })
+
+ it('checks last character not removed on paste, if not a delimiter', () => {
+ createInstance({ delimiterChars: [','], allowNew: false, suggestions: fixture })
+
+ paste('xxx, Thailand')
+
+ expect($('input').value).toEqual('xxx, Thailand')
+ })
})
describe('suggestions', () => {
@@ -291,7 +367,7 @@ describe('React Tags', () => {
type(query); click($('li[role="option"]:nth-child(2)'))
sinon.assert.calledOnce(props.handleAddition)
- sinon.assert.calledWith(props.handleAddition, { id: 196, name: 'United Kingdom' })
+ sinon.assert.calledWith(props.handleAddition, [{ id: 196, name: 'United Kingdom' }])
})
it('triggers addition for the selected suggestion when a delimiter is pressed', () => {
@@ -302,12 +378,12 @@ describe('React Tags', () => {
type(query); key('down', 'down', 'enter')
sinon.assert.calledOnce(props.handleAddition)
- sinon.assert.calledWith(props.handleAddition, { id: 196, name: 'United Kingdom' })
+ sinon.assert.calledWith(props.handleAddition, [{ id: 196, name: 'United Kingdom' }])
})
it('triggers addition for an unselected but matching suggestion when a delimiter is pressed', () => {
type('united kingdom'); key('enter')
- sinon.assert.calledWith(props.handleAddition, { id: 196, name: 'United Kingdom' })
+ sinon.assert.calledWith(props.handleAddition, [{ id: 196, name: 'United Kingdom' }])
})
it('clears the input when an addition is triggered', () => {
@@ -318,6 +394,41 @@ describe('React Tags', () => {
expect(input.value).toEqual('')
expect(document.activeElement).toEqual(input)
})
+
+ it('does nothing for onchange if there are no delimiterChars', () => {
+ createInstance({ delimiterChars: [] })
+
+ type('united kingdom,')
+
+ sinon.assert.notCalled(props.handleAddition)
+ })
+
+ it('checks to see if onchange accepts known tags, during paste', () => {
+ createInstance({
+ delimiterChars: [','],
+ allowNew: false,
+ handleAddition: props.handleAddition,
+ suggestions: fixture
+ })
+
+ paste('Thailand,')
+
+ sinon.assert.calledOnce(props.handleAddition)
+ sinon.assert.calledWith(props.handleAddition, [{ id: 184, name: 'Thailand' }])
+ })
+
+ it('checks to see if onchange rejects unknown tags, during paste', () => {
+ createInstance({
+ delimiterChars: [','],
+ allowNew: false,
+ handleAddition: props.handleAddition,
+ suggestions: fixture.map((item) => Object.assign({}, item, { disabled: true }))
+ })
+
+ paste('Algeria, abc,')
+
+ sinon.assert.notCalled(props.handleAddition)
+ })
})
describe('tags', () => {