Skip to content

Commit e5d8df7

Browse files
committed
Fixes i-like-robots#94 - Changes to support Android Chrome
1 parent d2770e7 commit e5d8df7

File tree

4 files changed

+124
-16
lines changed

4 files changed

+124
-16
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,11 @@ Boolean parameter to control whether the text-input should be automatically resi
121121

122122
#### delimiters (optional)
123123

124-
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).
124+
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).
125125

126126
#### delimiterChars (optional)
127127

128-
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: `[]`
128+
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: `[',', ' ']`
129129

130130
#### minQueryLength (optional)
131131

@@ -156,7 +156,7 @@ Override the default class names. Defaults:
156156

157157
#### handleAddition (required)
158158

159-
Function called when the user wants to add a tag. Receives the tag.
159+
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.
160160

161161
```js
162162
function (tag) {

example/main.js

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class App extends React.Component {
3333
return (
3434
<div>
3535
<Tags
36+
delimiterChars={[',', ' ']}
3637
tags={this.state.tags}
3738
suggestions={this.state.suggestions}
3839
handleDelete={this.handleDelete.bind(this)}

lib/ReactTags.js

+116-9
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,119 @@ class ReactTags extends React.Component {
4646
})
4747
}
4848

49+
/**
50+
* Protect against entered characters that could break a RegEx
51+
*/
52+
escapeForRegExp (query) {
53+
return query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&')
54+
}
55+
56+
isSuggestedTag(query) {
57+
58+
}
59+
60+
/**
61+
* Handles the value changes to the input field and uses the `delimiterChars`
62+
* property to know on what character to try to create a tag for. Only characters
63+
* valid for display in an `input` field are supported. Other values passed,
64+
* such as 'Tab' and 'Enter' cause adverse effects.
65+
*
66+
* Note, this method is necessary on Android, due to a limitation of the
67+
* `KeyboardEvent.key` having an undefined value, when using soft keyboards.
68+
* in Android's version of Google Chrome. This method also handles the paste
69+
* scenario, without needing to provide a supplemental 'onPaste' handl+er.
70+
*/
4971
handleChange (e) {
50-
const query = e.target.value
72+
const { delimiterChars } = this.props
5173

5274
if (this.props.handleInputChange) {
53-
this.props.handleInputChange(query)
75+
this.props.handleInputChange(e.target.value)
5476
}
5577

56-
this.setState({ query })
78+
const query = e.target.value
79+
80+
this.setState({ query: query })
81+
82+
if (query && delimiterChars.length > 0) {
83+
const regex = new RegExp('[' + this.escapeForRegExp(delimiterChars.join('')) + ']')
84+
85+
let tagsToAdd = []
86+
87+
// only process if query contains a delimiter character
88+
if (query.match(regex)) {
89+
// split the string based on the delimiterChars as a regex, being sure
90+
// to escape chars, to prevent them being treated as special characters
91+
const tags = query.split(regex)
92+
93+
// handle the case where the last character was not a delimiter, to
94+
// avoid matching text a user was not ready to lookup
95+
let maxTagIdx = tags.length
96+
if (delimiterChars.indexOf(query.charAt(query.length - 1)) < 0) {
97+
--maxTagIdx
98+
}
99+
100+
// deal with case where we don't allow new tags
101+
// for now just stop processing
102+
if (!this.props.allowNew) {
103+
const lastTag = tags[tags.length-2];
104+
const match = this.props.suggestions.findIndex((suggestion) => {
105+
return suggestion.name.toLowerCase() === lastTag.toLowerCase()
106+
})
107+
108+
if (match < 0) {
109+
this.setState({ query: query.substring(0, query.length - 1) })
110+
return
111+
}
112+
}
113+
114+
for (let i = 0; i < maxTagIdx; i++) {
115+
// the logic here is similar to handleKeyDown, but subtly different,
116+
// due to the context of the operation
117+
if (tags[i].length > 0) {
118+
// look to see if the tag is already known, ignoring case
119+
const matchIdx = this.props.suggestions.findIndex((suggestion) => {
120+
return tags[i].toLowerCase() === suggestion.name.toLowerCase()
121+
})
122+
123+
// if already known add it, otherwise add it only if we allow new tags
124+
if (matchIdx > -1) {
125+
tagsToAdd.push(this.props.suggestions[matchIdx])
126+
} else if (this.props.allowNew) {
127+
tagsToAdd.push({ name: tags[i] })
128+
}
129+
}
130+
}
131+
132+
// Add all the found tags. We do it one shot, to avoid potential
133+
// state issues.
134+
if (tagsToAdd.length > 0) {
135+
this.addTag(tagsToAdd)
136+
}
137+
138+
// if there was remaining undelimited text, add it to the query
139+
if (maxTagIdx < tags.length) {
140+
this.setState({ query: tags[maxTagIdx] })
141+
}
142+
}
143+
}
57144
}
58145

146+
/**
147+
* Handles the keydown event. This method allows handling of special keys,
148+
* such as tab, enter and other meta keys. Use the `delimiter` property
149+
* to define these keys.
150+
*
151+
* Note, While the `KeyboardEvent.keyCode` is considered deprecated, a limitation
152+
* in Android Chrome, related to soft keyboards, prevents us from using the
153+
* `KeyboardEvent.key` attribute. Any other scenario, not handled by this method,
154+
* and related to printable characters, is handled by the `handleChange()` method.
155+
*/
59156
handleKeyDown (e) {
60157
const { query, selectedIndex } = this.state
61-
const { delimiters, delimiterChars } = this.props
158+
const { delimiters } = this.props
62159

63160
// when one of the terminating keys is pressed, add current query to the tags.
64-
if (delimiters.indexOf(e.keyCode) > -1 || delimiterChars.indexOf(e.key) > -1) {
161+
if (delimiters.indexOf(e.keyCode) > -1) {
65162
if (query || selectedIndex > -1) {
66163
e.preventDefault()
67164
}
@@ -119,12 +216,22 @@ class ReactTags extends React.Component {
119216
this.setState({ focused: true })
120217
}
121218

122-
addTag (tag) {
123-
if (tag.disabled) {
219+
addTag (tags) {
220+
let filteredTags = tags;
221+
222+
if (!Array.isArray(filteredTags)) {
223+
filteredTags = [filteredTags];
224+
}
225+
226+
filteredTags = filteredTags.filter((tag) => {
227+
return tag.disabled !== true;
228+
});
229+
230+
if (filteredTags.length === 0) {
124231
return
125232
}
126233

127-
this.props.handleAddition(tag)
234+
this.props.handleAddition(filteredTags)
128235

129236
// reset the state
130237
this.setState({
@@ -194,7 +301,7 @@ ReactTags.defaultProps = {
194301
autofocus: true,
195302
autoresize: true,
196303
delimiters: [KEYS.TAB, KEYS.ENTER],
197-
delimiterChars: [],
304+
delimiterChars: [',', ' '],
198305
minQueryLength: 2,
199306
maxSuggestionsLength: 6,
200307
allowNew: false,

spec/ReactTags.spec.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe('React Tags', () => {
156156
type(query); key('enter')
157157

158158
sinon.assert.calledOnce(props.handleAddition)
159-
sinon.assert.calledWith(props.handleAddition, { name: query })
159+
sinon.assert.calledWith(props.handleAddition, [{ name: query }])
160160
})
161161

162162
it('can add new tags when a delimiter character is entered', () => {
@@ -291,7 +291,7 @@ describe('React Tags', () => {
291291
type(query); click($('li[role="option"]:nth-child(2)'))
292292

293293
sinon.assert.calledOnce(props.handleAddition)
294-
sinon.assert.calledWith(props.handleAddition, { id: 196, name: 'United Kingdom' })
294+
sinon.assert.calledWith(props.handleAddition, [{ id: 196, name: 'United Kingdom' }])
295295
})
296296

297297
it('triggers addition for the selected suggestion when a delimiter is pressed', () => {
@@ -302,12 +302,12 @@ describe('React Tags', () => {
302302
type(query); key('down', 'down', 'enter')
303303

304304
sinon.assert.calledOnce(props.handleAddition)
305-
sinon.assert.calledWith(props.handleAddition, { id: 196, name: 'United Kingdom' })
305+
sinon.assert.calledWith(props.handleAddition, [{ id: 196, name: 'United Kingdom' }])
306306
})
307307

308308
it('triggers addition for an unselected but matching suggestion when a delimiter is pressed', () => {
309309
type('united kingdom'); key('enter')
310-
sinon.assert.calledWith(props.handleAddition, { id: 196, name: 'United Kingdom' })
310+
sinon.assert.calledWith(props.handleAddition, [{ id: 196, name: 'United Kingdom' }])
311311
})
312312

313313
it('clears the input when an addition is triggered', () => {

0 commit comments

Comments
 (0)