From e5d8df769b5bba0dc606642c3ea065105d5c592c Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Tue, 26 Sep 2017 14:52:29 -0400 Subject: [PATCH] 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', () => {