Skip to content

Commit 9fa322b

Browse files
committed
Refactor filtered suggestions into top-level component
1 parent 26682f7 commit 9fa322b

File tree

3 files changed

+35
-42
lines changed

3 files changed

+35
-42
lines changed

lib/ReactTags.js

+23-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
33
import Tag from './Tag'
44
import Input from './Input'
55
import Suggestions from './Suggestions'
6+
import { escapeForRegExp } from './utils'
67

78
const KEYS = {
89
ENTER: 'Enter',
@@ -32,14 +33,17 @@ function pressDelimiterKey (e) {
3233

3334
if (this.state.query.length >= this.props.minQueryLength) {
3435
// Check if the user typed in an existing suggestion.
35-
const match = this.suggestions.state.options.findIndex((suggestion) => (
36-
suggestion.name.search(new RegExp(`^${this.state.query}$`, 'i')) === 0
37-
))
36+
const match = this.state.options.findIndex((option) => {
37+
const query = escapeForRegExp(this.state.query)
38+
const regex = new RegExp(`^${query}$`, 'i')
39+
40+
return regex.test(option.name)
41+
})
3842

3943
const index = this.state.selected === -1 ? match : this.state.selected
4044

4145
if (index > -1) {
42-
this.addTag(this.suggestions.state.options[index])
46+
this.addTag(this.state.options[index])
4347
} else if (this.props.allowNew) {
4448
this.addTag({ name: this.state.query })
4549
}
@@ -51,7 +55,7 @@ function pressUpKey (e) {
5155

5256
// if first item, cycle to the bottom
5357
if (this.state.selected <= 0) {
54-
this.setState({ selected: this.suggestions.state.options.length - 1 })
58+
this.setState({ selected: this.state.options.length - 1 })
5559
} else {
5660
this.setState({ selected: this.state.selected - 1 })
5761
}
@@ -61,7 +65,7 @@ function pressDownKey (e) {
6165
e.preventDefault()
6266

6367
// if last item, cycle to top
64-
if (this.state.selected >= this.suggestions.state.options.length - 1) {
68+
if (this.state.selected >= this.state.options.length - 1) {
6569
this.setState({ selected: 0 })
6670
} else {
6771
this.setState({ selected: this.state.selected + 1 })
@@ -75,13 +79,19 @@ function pressBackspaceKey () {
7579
}
7680
}
7781

82+
function filterSuggestions (query, suggestions) {
83+
const regexp = new RegExp(`(?:^|\\s)${escapeForRegExp(query)}`, 'i')
84+
return suggestions.filter((item) => regexp.test(item.name))
85+
}
86+
7887
class ReactTags extends React.Component {
7988
constructor (props) {
8089
super(props)
8190

8291
this.state = {
8392
query: '',
8493
focused: false,
94+
options: [],
8595
selected: -1,
8696
classNames: Object.assign({}, CLASS_NAMES, this.props.classNames)
8797
}
@@ -100,7 +110,12 @@ class ReactTags extends React.Component {
100110
this.props.onInput(query)
101111
}
102112

103-
this.setState({ query })
113+
if (query !== this.state.query) {
114+
const filtered = filterSuggestions(query, this.props.suggestions)
115+
const options = filtered.slice(0, this.props.maxSuggestionsLength)
116+
117+
this.setState({ query, options })
118+
}
104119
}
105120

106121
onKeyDown (e) {
@@ -203,9 +218,7 @@ class ReactTags extends React.Component {
203218
ref={(c) => { this.suggestions = c }}
204219
listboxId={listboxId}
205220
expanded={expanded}
206-
suggestions={this.props.suggestions}
207-
addTag={this.addTag.bind(this)}
208-
maxSuggestionsLength={this.props.maxSuggestionsLength} />
221+
addTag={this.addTag.bind(this)} />
209222
</div>
210223
</div>
211224
)

lib/Suggestions.js

+7-32
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,24 @@
11
import React from 'react'
2+
import { escapeForRegExp } from './utils'
23

3-
function escapeForRegExp (query) {
4-
return query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&')
5-
}
6-
7-
function markIt (input, query) {
8-
const regex = RegExp(escapeForRegExp(query), 'gi')
9-
10-
return {
11-
__html: input.replace(regex, '<mark>$&</mark>')
12-
}
13-
}
14-
15-
function filterSuggestions (query, suggestions, length) {
16-
const regex = new RegExp(`(?:^|\\s)${escapeForRegExp(query)}`, 'i')
17-
return suggestions.filter((item) => regex.test(item.name)).slice(0, length)
4+
function markIt (name, query) {
5+
const regexp = new RegExp(escapeForRegExp(query), 'gi')
6+
return name.replace(regexp, '<mark>$&</mark>')
187
}
198

209
class Suggestions extends React.Component {
21-
constructor (props) {
22-
super(props)
23-
24-
this.state = {
25-
options: filterSuggestions(this.props.query, this.props.suggestions, this.props.maxSuggestionsLength)
26-
}
27-
}
28-
29-
componentWillReceiveProps (newProps) {
30-
this.setState({
31-
options: filterSuggestions(newProps.query, newProps.suggestions, newProps.maxSuggestionsLength)
32-
})
33-
}
34-
3510
onMouseDown (item, e) {
3611
// focus is shifted on mouse down but calling preventDefault prevents this
3712
e.preventDefault()
3813
this.props.addTag(item)
3914
}
4015

4116
render () {
42-
if (!this.props.expanded || !this.state.options.length) {
17+
if (!this.props.expanded || !this.props.options.length) {
4318
return null
4419
}
4520

46-
const options = this.state.options.map((item, i) => {
21+
const options = this.props.options.map((item, i) => {
4722
const key = `${this.props.listboxId}-${i}`
4823
const classNames = []
4924

@@ -63,7 +38,7 @@ class Suggestions extends React.Component {
6338
className={classNames.join(' ')}
6439
aria-disabled={item.disabled === true}
6540
onMouseDown={this.onMouseDown.bind(this, item)}>
66-
<span dangerouslySetInnerHTML={markIt(item.name, this.props.query)} />
41+
<span dangerouslySetInnerHTML={{ __html: markIt(item.name, this.props.query) }} />
6742
</li>
6843
)
6944
})

lib/utils.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function escapeForRegExp (string) {
2+
return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&')
3+
}
4+
5+
module.exports = { escapeForRegExp }

0 commit comments

Comments
 (0)