From e5d8df769b5bba0dc606642c3ea065105d5c592c Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Tue, 26 Sep 2017 14:52:29 -0400 Subject: [PATCH 1/8] Fixes #94 - Changes to support Android Chrome --- README.md | 6 +- example/main.js | 1 + lib/ReactTags.js | 125 ++++++++++++++++++++++++++++++++++++++--- spec/ReactTags.spec.js | 8 +-- 4 files changed, 124 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2860b92..795ba54 100644 --- a/README.md +++ b/README.md @@ -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 + const tags = query.split(regex) + + // 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) { + const lastTag = tags[tags.length-2]; + const match = this.props.suggestions.findIndex((suggestion) => { + return suggestion.name.toLowerCase() === lastTag.toLowerCase() + }) + + if (match < 0) { + this.setState({ query: query.substring(0, query.length - 1) }) + 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 + if (tags[i].length > 0) { + // look to see if the tag is already known, ignoring case + const matchIdx = this.props.suggestions.findIndex((suggestion) => { + return tags[i].toLowerCase() === suggestion.name.toLowerCase() + }) + + // if already known add it, otherwise add it only if we allow new tags + if (matchIdx > -1) { + tagsToAdd.push(this.props.suggestions[matchIdx]) + } else if (this.props.allowNew) { + tagsToAdd.push({ name: tags[i] }) + } + } + } + + // Add all the found tags. We do it one shot, to avoid potential + // state issues. + if (tagsToAdd.length > 0) { + this.addTag(tagsToAdd) + } + + // if there was remaining undelimited text, add it to the query + if (maxTagIdx < tags.length) { + this.setState({ query: tags[maxTagIdx] }) + } + } + } } + /** + * 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 +216,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 +301,7 @@ ReactTags.defaultProps = { autofocus: true, autoresize: true, delimiters: [KEYS.TAB, KEYS.ENTER], - delimiterChars: [], + delimiterChars: [',', ' '], minQueryLength: 2, maxSuggestionsLength: 6, allowNew: false, diff --git a/spec/ReactTags.spec.js b/spec/ReactTags.spec.js index 947443e..3b1dee9 100644 --- a/spec/ReactTags.spec.js +++ b/spec/ReactTags.spec.js @@ -156,7 +156,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', () => { @@ -291,7 +291,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 +302,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', () => { From 8898a519d705071bb128d803f67bc11d1a89f069 Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Tue, 26 Sep 2017 15:00:51 -0400 Subject: [PATCH 2/8] resolve issues causing lint failure --- lib/ReactTags.js | 22 +++++++++++----------- package.json | 16 ++++++++-------- webpack.config.js | 27 ++++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/lib/ReactTags.js b/lib/ReactTags.js index 6a72faf..9cf8f37 100644 --- a/lib/ReactTags.js +++ b/lib/ReactTags.js @@ -53,8 +53,8 @@ class ReactTags extends React.Component { return query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') } - isSuggestedTag(query) { - + isSuggestedTag (query) { + } /** @@ -100,15 +100,15 @@ class ReactTags extends React.Component { // deal with case where we don't allow new tags // for now just stop processing if (!this.props.allowNew) { - const lastTag = tags[tags.length-2]; + const lastTag = tags[tags.length - 2] const match = this.props.suggestions.findIndex((suggestion) => { return suggestion.name.toLowerCase() === lastTag.toLowerCase() }) - + if (match < 0) { - this.setState({ query: query.substring(0, query.length - 1) }) + this.setState({ query: query.substring(0, query.length - 1) }) return - } + } } for (let i = 0; i < maxTagIdx; i++) { @@ -217,15 +217,15 @@ class ReactTags extends React.Component { } addTag (tags) { - let filteredTags = tags; - + let filteredTags = tags + if (!Array.isArray(filteredTags)) { - filteredTags = [filteredTags]; + filteredTags = [filteredTags] } filteredTags = filteredTags.filter((tag) => { - return tag.disabled !== true; - }); + return tag.disabled !== true + }) if (filteredTags.length === 0) { return diff --git a/package.json b/package.json index 13fda52..a25a2b9 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "test": "jasmine", "coverage": "istanbul cover -i 'dist-es5/**' jasmine", "lint": "standard lib/*.js spec/*.js", - "dev": "webpack-dev-server --progress --colors", - "build:example": "webpack -p", - "build:es5": "buble lib -o dist-es5", - "build:es6": "buble lib -o dist-es6 -t node:6", + "dev": "webpack-dev-server --progress --colors --port ${PORT:-8080} --host 0.0.0.0", + "build:example": "webpack -p --config-name example", + "build:es5": "buble lib --objectAssign -o dist-es5", + "build:es6": "buble lib --objectAssign -o dist-es6 -t node:6", "prepublish": "npm run build:es5 && npm run build:es6" }, "files": [ @@ -44,8 +44,8 @@ "react-dom": "^15.0.0" }, "devDependencies": { - "buble": "^0.12.5", - "buble-loader": "^0.2.2", + "buble": "^0.16.0", + "buble-loader": "^0.4.1", "coveralls": "^2.11.12", "istanbul": "^0.4.4", "jasmine": "^2.4.1", @@ -56,7 +56,7 @@ "react-dom": "^15.5.0", "sinon": "^1.17.5", "standard": "^7.1.2", - "webpack": "^1.9.4", - "webpack-dev-server": "^1.8.2" + "webpack": "^3.6.0", + "webpack-dev-server": "^2.8.2" } } diff --git a/webpack.config.js b/webpack.config.js index a7e8bd0..3f704bf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,11 +1,12 @@ const webpack = require('webpack') -module.exports = { +module.exports = [{ + name: 'example', entry: './example/main.js', devtool: 'source-map', module: { loaders: [ - { test: /\.js$/, loader: 'buble', exclude: /node_modules/ } + { test: /\.js$/, loader: 'buble-loader?objectAssign=Object.assign', exclude: /node_modules/ } ] }, plugins: [ @@ -22,4 +23,24 @@ module.exports = { // 'react-dom': 'preact-compat' // } // } -} +}, { + name: 'es5', + entry: './lib/ReactTags.js', + output: { + filename: 'dist-es5/ReactTags.js' + }, + module: { + loaders: [{ + test: /\.js$/, + loader: 'babel-loader', + exclude: [/node_modules/], + options: { + presets: [ + 'babel-preset-es2015', + 'babel-preset-react', + 'babel-preset-stage-0', + ].map(require.resolve), + } + }] + } +}] From 7498d30e7a21999bb970705c2501ff9b49f6c9c9 Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Tue, 26 Sep 2017 15:23:28 -0400 Subject: [PATCH 3/8] Removed space delimiter as default --- lib/ReactTags.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ReactTags.js b/lib/ReactTags.js index 9cf8f37..a373a27 100644 --- a/lib/ReactTags.js +++ b/lib/ReactTags.js @@ -301,7 +301,7 @@ ReactTags.defaultProps = { autofocus: true, autoresize: true, delimiters: [KEYS.TAB, KEYS.ENTER], - delimiterChars: [',', ' '], + delimiterChars: [','], minQueryLength: 2, maxSuggestionsLength: 6, allowNew: false, From 8823bd3646f6bdf5a26cf61cf8b479db208b641b Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Tue, 26 Sep 2017 15:26:03 -0400 Subject: [PATCH 4/8] Updated TOC in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 795ba54..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) @@ -125,7 +125,7 @@ Array of integers matching keyboard event `keyCode` values. When a corresponding #### delimiterChars (optional) -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: `[',', ' ']` +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) From 5987cabd57c869a28703c89e1a0a6236797bd374 Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Tue, 26 Sep 2017 15:31:36 -0400 Subject: [PATCH 5/8] Reverting files not meant for push --- package.json | 16 ++++++++-------- webpack.config.js | 27 +++------------------------ 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index a25a2b9..13fda52 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "test": "jasmine", "coverage": "istanbul cover -i 'dist-es5/**' jasmine", "lint": "standard lib/*.js spec/*.js", - "dev": "webpack-dev-server --progress --colors --port ${PORT:-8080} --host 0.0.0.0", - "build:example": "webpack -p --config-name example", - "build:es5": "buble lib --objectAssign -o dist-es5", - "build:es6": "buble lib --objectAssign -o dist-es6 -t node:6", + "dev": "webpack-dev-server --progress --colors", + "build:example": "webpack -p", + "build:es5": "buble lib -o dist-es5", + "build:es6": "buble lib -o dist-es6 -t node:6", "prepublish": "npm run build:es5 && npm run build:es6" }, "files": [ @@ -44,8 +44,8 @@ "react-dom": "^15.0.0" }, "devDependencies": { - "buble": "^0.16.0", - "buble-loader": "^0.4.1", + "buble": "^0.12.5", + "buble-loader": "^0.2.2", "coveralls": "^2.11.12", "istanbul": "^0.4.4", "jasmine": "^2.4.1", @@ -56,7 +56,7 @@ "react-dom": "^15.5.0", "sinon": "^1.17.5", "standard": "^7.1.2", - "webpack": "^3.6.0", - "webpack-dev-server": "^2.8.2" + "webpack": "^1.9.4", + "webpack-dev-server": "^1.8.2" } } diff --git a/webpack.config.js b/webpack.config.js index 3f704bf..a7e8bd0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,12 +1,11 @@ const webpack = require('webpack') -module.exports = [{ - name: 'example', +module.exports = { entry: './example/main.js', devtool: 'source-map', module: { loaders: [ - { test: /\.js$/, loader: 'buble-loader?objectAssign=Object.assign', exclude: /node_modules/ } + { test: /\.js$/, loader: 'buble', exclude: /node_modules/ } ] }, plugins: [ @@ -23,24 +22,4 @@ module.exports = [{ // 'react-dom': 'preact-compat' // } // } -}, { - name: 'es5', - entry: './lib/ReactTags.js', - output: { - filename: 'dist-es5/ReactTags.js' - }, - module: { - loaders: [{ - test: /\.js$/, - loader: 'babel-loader', - exclude: [/node_modules/], - options: { - presets: [ - 'babel-preset-es2015', - 'babel-preset-react', - 'babel-preset-stage-0', - ].map(require.resolve), - } - }] - } -}] +} From dc283b3c2175eef70fd37ef05f9730c63379cc49 Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Tue, 26 Sep 2017 18:17:53 -0400 Subject: [PATCH 6/8] Fixing coverage and issues revealed --- lib/ReactTags.js | 25 ++++++++--------- package.json | 2 +- spec/ReactTags.spec.js | 64 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/lib/ReactTags.js b/lib/ReactTags.js index a373a27..79d39d7 100644 --- a/lib/ReactTags.js +++ b/lib/ReactTags.js @@ -40,6 +40,7 @@ class ReactTags extends React.Component { } } + /* istanbul ignore next: sanity check */ componentWillReceiveProps (newProps) { this.setState({ classNames: Object.assign({}, CLASS_NAMES, newProps.classNames) @@ -53,10 +54,6 @@ class ReactTags extends React.Component { return query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') } - isSuggestedTag (query) { - - } - /** * Handles the value changes to the input field and uses the `delimiterChars` * property to know on what character to try to create a tag for. Only characters @@ -71,6 +68,7 @@ class ReactTags extends React.Component { handleChange (e) { const { delimiterChars } = this.props + /* istanbul ignore else: sanity check */ if (this.props.handleInputChange) { this.props.handleInputChange(e.target.value) } @@ -100,7 +98,8 @@ class ReactTags extends React.Component { // deal with case where we don't allow new tags // for now just stop processing if (!this.props.allowNew) { - const lastTag = tags[tags.length - 2] + let lastTag = tags[tags.length - 2] + const match = this.props.suggestions.findIndex((suggestion) => { return suggestion.name.toLowerCase() === lastTag.toLowerCase() }) @@ -110,34 +109,34 @@ class ReactTags extends React.Component { 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 - if (tags[i].length > 0) { + 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 tags[i].toLowerCase() === suggestion.name.toLowerCase() + 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: tags[i] }) + tagsToAdd.push({ name: tag.trim() }) } } } // Add all the found tags. We do it one shot, to avoid potential // state issues. - if (tagsToAdd.length > 0) { - this.addTag(tagsToAdd) - } + this.addTag(tagsToAdd) // if there was remaining undelimited text, add it to the query if (maxTagIdx < tags.length) { - this.setState({ query: tags[maxTagIdx] }) + this.setState({ query: tags[maxTagIdx].trim() }) } } } 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 3b1dee9..d0de15f 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 }) @@ -166,6 +172,28 @@ 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' }]) + }) }) describe('suggestions', () => { @@ -318,6 +346,42 @@ 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', () => { From 6f7e8413f989f26d1c1c339579ba75332167fca7 Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Tue, 26 Sep 2017 18:25:39 -0400 Subject: [PATCH 7/8] fix issues breaking test --- lib/ReactTags.js | 4 ++-- spec/ReactTags.spec.js | 33 ++++++++++++++++----------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/ReactTags.js b/lib/ReactTags.js index 79d39d7..056290f 100644 --- a/lib/ReactTags.js +++ b/lib/ReactTags.js @@ -109,11 +109,11 @@ class ReactTags extends React.Component { 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(); + 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) => { diff --git a/spec/ReactTags.spec.js b/spec/ReactTags.spec.js index d0de15f..8934d5b 100644 --- a/spec/ReactTags.spec.js +++ b/spec/ReactTags.spec.js @@ -53,7 +53,7 @@ function type (value) { } function paste (value) { - $('input').value = value; + $('input').value = value // React calls onchange following paste TestUtils.Simulate.change($('input')) } @@ -179,21 +179,21 @@ describe('React Tags', () => { const input = $('input') paste('antarctica, spain') - + sinon.assert.calledOnce(props.handleAddition) - sinon.assert.calledWith(props.handleAddition, [{ name: 'antarctica' }]) - - expect(input.value).toEqual('spain') + 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' }]) - }) + sinon.assert.calledWith(props.handleAddition, [{ name: 'Algeria' }, { name: 'Guinea Bissau' }]) + }) }) describe('suggestions', () => { @@ -356,9 +356,9 @@ describe('React Tags', () => { }) it('checks to see if onchange accepts known tags, during paste', () => { - createInstance({ - delimiterChars: [','], - allowNew: false, + createInstance({ + delimiterChars: [','], + allowNew: false, handleAddition: props.handleAddition, suggestions: fixture }) @@ -367,13 +367,12 @@ describe('React Tags', () => { 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, + createInstance({ + delimiterChars: [','], + allowNew: false, handleAddition: props.handleAddition, suggestions: fixture.map((item) => Object.assign({}, item, { disabled: true })) }) @@ -381,7 +380,7 @@ describe('React Tags', () => { paste('Algeria, abc,') sinon.assert.notCalled(props.handleAddition) - }) + }) }) describe('tags', () => { From 76696cf2e021a911d07caaa599bb7aaa468ce666 Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Wed, 27 Sep 2017 14:02:15 -0400 Subject: [PATCH 8/8] tuning of typing vs paste inconsistency --- lib/ReactTags.js | 19 +++++++++++------ spec/ReactTags.spec.js | 48 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/lib/ReactTags.js b/lib/ReactTags.js index 056290f..d2f07f1 100644 --- a/lib/ReactTags.js +++ b/lib/ReactTags.js @@ -86,7 +86,10 @@ class ReactTags extends React.Component { 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 - const tags = query.split(regex) + // 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 @@ -95,17 +98,21 @@ class ReactTags extends React.Component { --maxTagIdx } - // deal with case where we don't allow new tags - // for now just stop processing + // deal with case where we don't allow new tags, for now just stop processing if (!this.props.allowNew) { - let lastTag = tags[tags.length - 2] + let lastTag = tags[maxTagIdx - 1] const match = this.props.suggestions.findIndex((suggestion) => { - return suggestion.name.toLowerCase() === lastTag.toLowerCase() + return suggestion.name.toLowerCase() === lastTag.trim().toLowerCase() }) if (match < 0) { - this.setState({ query: query.substring(0, query.length - 1) }) + 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 } } diff --git a/spec/ReactTags.spec.js b/spec/ReactTags.spec.js index 8934d5b..f50074f 100644 --- a/spec/ReactTags.spec.js +++ b/spec/ReactTags.spec.js @@ -194,6 +194,54 @@ describe('React Tags', () => { 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', () => {