-
Notifications
You must be signed in to change notification settings - Fork 170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes #94 - Changes to support Android Chrome #97
base: main
Are you sure you want to change the base?
Changes from all commits
e5d8df7
8898a51
7498d30
8823bd3
5987cab
dc283b3
6f7e841
76696cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,7 +73,7 @@ React.render(<App />, 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this could be considered a breaking change, it might better to invoke the callback once for each tag to avoid that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is, but I couldn't seem to get things to behave otherwise, when I was testing with the example. I can make another branch in my fork to show you the issue? The issue happens when a paste is made. If I do a single call then then tags stay as should, but if I do multiple calls we lose the intermediate tags. For example, try pasting
This is not an issue in normal times. Here is the other branch to check behaviour with: https://github.com/ajmas/react-tags/tree/no-addtag-array |
||
|
||
```js | ||
function (tag) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,28 +40,131 @@ class ReactTags extends React.Component { | |
} | ||
} | ||
|
||
/* istanbul ignore next: sanity check */ | ||
componentWillReceiveProps (newProps) { | ||
this.setState({ | ||
classNames: Object.assign({}, CLASS_NAMES, newProps.classNames) | ||
}) | ||
} | ||
|
||
/** | ||
* Protect against entered characters that could break a RegEx | ||
*/ | ||
escapeForRegExp (query) { | ||
return query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') | ||
} | ||
|
||
/** | ||
* 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 | ||
* valid for display in an `input` field are supported. Other values passed, | ||
* such as 'Tab' and 'Enter' cause adverse effects. | ||
* | ||
* Note, this method is necessary on Android, due to a limitation of the | ||
* `KeyboardEvent.key` having an undefined value, when using soft keyboards. | ||
* in Android's version of Google Chrome. This method also handles the paste | ||
* scenario, without needing to provide a supplemental 'onPaste' handl+er. | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the description! |
||
handleChange (e) { | ||
const query = e.target.value | ||
const { delimiterChars } = this.props | ||
|
||
/* istanbul ignore else: sanity check */ | ||
if (this.props.handleInputChange) { | ||
this.props.handleInputChange(query) | ||
this.props.handleInputChange(e.target.value) | ||
} | ||
|
||
this.setState({ query }) | ||
const query = e.target.value | ||
|
||
this.setState({ query: query }) | ||
|
||
if (query && delimiterChars.length > 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, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice distinction 👍