Skip to content

Commit

Permalink
Fixes i-like-robots#94 - Changes to support Android Chrome
Browse files Browse the repository at this point in the history
  • Loading branch information
ajmas committed Sep 26, 2017
1 parent d2770e7 commit e5d8df7
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 16 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions example/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class App extends React.Component {
return (
<div>
<Tags
delimiterChars={[',', ' ']}
tags={this.state.tags}
suggestions={this.state.suggestions}
handleDelete={this.handleDelete.bind(this)}
Expand Down
125 changes: 116 additions & 9 deletions lib/ReactTags.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,119 @@ class ReactTags extends React.Component {
})
}

/**
* Protect against entered characters that could break a RegEx
*/
escapeForRegExp (query) {
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
* 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.
*/
handleChange (e) {
const query = e.target.value
const { delimiterChars } = this.props

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
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()
}
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -194,7 +301,7 @@ ReactTags.defaultProps = {
autofocus: true,
autoresize: true,
delimiters: [KEYS.TAB, KEYS.ENTER],
delimiterChars: [],
delimiterChars: [',', ' '],
minQueryLength: 2,
maxSuggestionsLength: 6,
allowNew: false,
Expand Down
8 changes: 4 additions & 4 deletions spec/ReactTags.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down

0 comments on commit e5d8df7

Please sign in to comment.