Skip to content

fixes issue #84, for pasting with delimiters #90

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

Closed
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Array of integers matching keyboard event `keyCode` values. When a corresponding

#### 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 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. Note, that specifying delimiters longer than one character is only supported for paste operations. Example usage: `delimiterChars={[',', ' ']}`. Default: `['\t', '\r\n', '\r', '\n']`

#### minQueryLength (optional)

Expand Down
4 changes: 3 additions & 1 deletion example/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ class App extends React.Component {
return (
<div>
<Tags
delimiterChars={[',', ' ', '\t']}
tags={this.state.tags}
suggestions={this.state.suggestions}
handleDelete={this.handleDelete.bind(this)}
handleAddition={this.handleAddition.bind(this)} />
handleAddition={this.handleAddition.bind(this)}
/>
<hr />
<pre><code>{JSON.stringify(this.state.tags, null, 2)}</code></pre>
</div>
Expand Down
54 changes: 51 additions & 3 deletions lib/ReactTags.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class ReactTags extends React.Component {
})
}

escapeForRegExp (query) {
return query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&')
}

handleChange (e) {
const query = e.target.value

Expand All @@ -56,6 +60,47 @@ class ReactTags extends React.Component {
this.setState({ query })
}

handlePaste (e) {
// allow over-ride, if there is a need
if (this.props.handlePaste) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still advocate sticking with React conventions and calling this onPaste, but take that as just an opinion of a fellow react component dev rather than anything more than that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See issue #91

return this.props.handlePaste(e)
}

const { delimiterChars } = this.props

e.preventDefault()

// get the text data from the clipboard
const data = e.clipboardData.getData('Text')

if (data && delimiterChars.length > 0) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the expectation if delimiterChars.length is 0? Currently nothing is converted to tag. I am want to see if this is reasonable or whether we should treat it as one block?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are no delimiterChars, then ReactTags can't do its job, I think? It needs a list to be able to chunk up input in the first place so if it does not have that list it can't convert string data into tags. Some options are to issue a warning ("no delimiter characters specified for onPaste handling by ReactTags") or to simply break out of the handler function before any code actually gets run.

@i-like-robots is the idea that delimiterChars and delimiters are mutually exclusive (e.g. if you need real letters, don't use numbers, and vice versa) or are they complementary? (in which case I might try to build a map that can tell which key is which keyboard event code, because the difference between those two is still ridiculous =D)

Copy link
Contributor Author

@ajmas ajmas Aug 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be okay:

else if (!delimiterChars || delimiterChars.length === 0) {
      if (console) {
        console.warn('no delimiterChars specified, so ignoring paste operation')
      }
    }

For the evolution of delimiterChars and delimiters, could that be another ticket, given there is probably a lot of subtle use cases? For example the enter key can have two different values, depending on whether it is on the alphanumeric or num-pad section of the keyboard. I don't have a num-pad to test with.

Should that unification work be done as part of issue #81?

// 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 = data.split(new RegExp('[' + this.escapeForRegExp(delimiterChars.join('')) + ']'))
for (let i = 0; i < tags.length; 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
const matchIdx = this.props.suggestions.findIndex((suggestion) => {
return tags[i] === suggestion.name
})

// if already known add it, otherwise add it only if we allow new tags
if (matchIdx > -1) {
this.addTag(this.props.suggestions[matchIdx])
} else if (this.props.allowNew) {
this.addTag({ name: tags[i] })
}
}
}
} else if (!delimiterChars || delimiterChars.length === 0) {
if (console) {
console.warn('no delimiterChars specified, so ignoring paste operation')
}
}
}

handleKeyDown (e) {
const { query, selectedIndex } = this.state
const { delimiters, delimiterChars } = this.props
Expand Down Expand Up @@ -166,14 +211,16 @@ class ReactTags extends React.Component {
onBlur={this.handleBlur.bind(this)}
onFocus={this.handleFocus.bind(this)}
onChange={this.handleChange.bind(this)}
onKeyDown={this.handleKeyDown.bind(this)}>
onKeyDown={this.handleKeyDown.bind(this)}
onPaste={this.handlePaste.bind(this)} >
<Input {...this.state}
ref={(c) => { this.input = c }}
listboxId={listboxId}
autofocus={this.props.autofocus}
autoresize={this.props.autoresize}
expandable={expandable}
placeholder={this.props.placeholder} />
placeholder={this.props.placeholder}
/>
<Suggestions {...this.state}
ref={(c) => { this.suggestions = c }}
listboxId={listboxId}
Expand All @@ -194,7 +241,7 @@ ReactTags.defaultProps = {
autofocus: true,
autoresize: true,
delimiters: [KEYS.TAB, KEYS.ENTER],
delimiterChars: [],
delimiterChars: ['\t', '\r\n', '\r', '\n'],
minQueryLength: 2,
maxSuggestionsLength: 6,
allowNew: false,
Expand All @@ -213,6 +260,7 @@ ReactTags.propTypes = {
handleDelete: PropTypes.func.isRequired,
handleAddition: PropTypes.func.isRequired,
handleInputChange: PropTypes.func,
handlePaste: PropTypes.func,
minQueryLength: PropTypes.number,
maxSuggestionsLength: PropTypes.number,
classNames: PropTypes.object,
Expand Down
117 changes: 117 additions & 0 deletions spec/ReactTags.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ function click (target) {
TestUtils.Simulate.click(target)
}

function paste (target, eventData) {
TestUtils.Simulate.paste(target, eventData)
}

describe('React Tags', () => {
afterEach(() => {
teardownInstance()
Expand Down Expand Up @@ -371,6 +375,100 @@ describe('React Tags', () => {

expect($$('.custom-tag').length).toEqual(2)
})

it('can receive tags through paste, respecting default delimiter chars', () => {
// The large range of delimiterChars in the test is to ensure
// they don't take on new meaning when used as part of a regex.
// Also ensure we accept multicharacter separators, in this scenario
createInstance({
allowNew: true
})

paste($('input'), { clipboardData: {
types: ['text/plain', 'Text'],
getData: (type) => 'foo\tbar\r\nbaz\rfam\nbam'
}})

sinon.assert.callCount(props.handleAddition, 5)
})

it('can receive tags through paste, respecting delimiter chars', () => {
// The large range of delimiterChars in the test is to ensure
// they don't take on new meaning when used as part of a regex.
// Also ensure we accept multicharacter separators, in this scenario
createInstance({
allowNew: true,
delimiterChars: ['^', ',', ';', '.', '\\', '[', ']X'],
suggestions: [{ id: 1, name: 'foo' }]
})

paste($('input'), { clipboardData: {
types: ['text/plain', 'Text'],
getData: (type) => 'foo,bar;baz^fam\\bam[moo]Xark'
}})

sinon.assert.callCount(props.handleAddition, 7)
})

it('ignores paste operation, if no delimiterChars are specified', () => {
// The large range of delimiterChars in the test is to ensure
// they don't take on new meaning when used as part of a regex.
// Also ensure we accept multicharacter separators, in this scenario
createInstance({
allowNew: true,
delimiterChars: []
})

paste($('input'), { clipboardData: {
types: ['text/plain', 'Text'],
getData: (type) => 'foo\tbar\r\nbaz\rfam\nbam'
}})

sinon.assert.callCount(props.handleAddition, 0)
})

it('can receive tags through paste, ignoring new tags', () => {
createInstance({
allowNew: false,
delimiterChars: [','],
suggestions: [{ id: 1, name: 'foo' }]
})

paste($('input'), { clipboardData: {
types: ['text/plain', 'Text'],
getData: (type) => 'foo,bar'
}})

sinon.assert.callCount(props.handleAddition, 1)
})

it('accepts no paste, if there are not delimiterChars', () => {
createInstance({
allowNew: true,
delimiterChars: []
})

paste($('input'), { clipboardData: {
types: ['text/plain', 'Text'],
getData: (type) => 'foo,bar;baz'
}})

sinon.assert.callCount(props.handleAddition, 0)
})

it('adds no tags, if the pasted string is empty', () => {
createInstance({
allowNew: true,
delimiterChars: [',']
})

paste($('input'), { clipboardData: {
types: ['text/plain', 'Text'],
getData: (type) => ''
}})

sinon.assert.callCount(props.handleAddition, 0)
})
})

describe('sizer', () => {
Expand Down Expand Up @@ -435,4 +533,23 @@ describe('React Tags', () => {
expect(input.style.width).toBeFalsy()
})
})

describe('event override', () => {
it('can receive tags through paste, respecting delimiters', () => {
createInstance({
allowNew: true,
delimiterChars: [','],
handlePaste: (e) => {
e.preventDefault()
}
})

paste($('input'), { clipboardData: {
types: ['text/plain', 'Text'],
getData: (type) => 'foo,bar;baz'
}})

sinon.assert.callCount(props.handleAddition, 0)
})
})
})
7 changes: 7 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ module.exports = {
output: {
filename: 'example/bundle.js'
},
devServer: {
disableHostCheck: true,
// listen server to be accessed from another host
// useful for mobile testing
host: "0.0.0.0",
port: process.env.PORT || 8080
}
// resolve: {
// alias: {
// 'react': 'preact-compat',
Expand Down