Skip to content

Commit 64966a0

Browse files
Merge pull request #115 from i-like-robots/refactor-suggestions
Refactor suggestions
2 parents a7c014e + 9e79421 commit 64966a0

File tree

4 files changed

+93
-79
lines changed

4 files changed

+93
-79
lines changed

lib/Input.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class Input extends React.Component {
6363
}
6464

6565
render () {
66-
const { query, placeholder, expanded, listboxId, selected } = this.props
66+
const { query, placeholder, expanded, listboxId, index } = this.props
6767

6868
return (
6969
<div className={this.props.classNames.searchInput}>
@@ -75,7 +75,7 @@ class Input extends React.Component {
7575
aria-autocomplete='list'
7676
aria-label={placeholder}
7777
aria-owns={listboxId}
78-
aria-activedescendant={selected > -1 ? `${listboxId}-${selected}` : null}
78+
aria-activedescendant={index > -1 ? `${listboxId}-${index}` : null}
7979
aria-expanded={expanded}
8080
style={{ width: this.state.inputWidth }} />
8181
<div ref={(c) => { this.sizer = c }} style={SIZER_STYLES}>{query || placeholder}</div>

lib/ReactTags.js

Lines changed: 68 additions & 44 deletions
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 { matchExact, matchPartial } from './concerns/matchers'
67

78
const KEYS = {
89
ENTER: 'Enter',
@@ -25,15 +26,64 @@ const CLASS_NAMES = {
2526
suggestionDisabled: 'is-disabled'
2627
}
2728

29+
function pressDelimiterKey (e) {
30+
if (this.state.query || this.state.index > -1) {
31+
e.preventDefault()
32+
}
33+
34+
if (this.state.query.length >= this.props.minQueryLength) {
35+
// Check if the user typed in an existing suggestion.
36+
const match = this.state.options.findIndex((option) => {
37+
return matchExact(this.state.query).test(option.name)
38+
})
39+
40+
const index = this.state.index === -1 ? match : this.state.index
41+
42+
if (index > -1) {
43+
this.addTag(this.state.options[index])
44+
} else if (this.props.allowNew) {
45+
this.addTag({ name: this.state.query })
46+
}
47+
}
48+
}
49+
50+
function pressUpKey (e) {
51+
e.preventDefault()
52+
53+
// if first item, cycle to the bottom
54+
const size = this.state.options.length - 1
55+
this.setState({ index: this.state.index <= 0 ? size : this.state.index - 1 })
56+
}
57+
58+
function pressDownKey (e) {
59+
e.preventDefault()
60+
61+
// if last item, cycle to top
62+
const size = this.state.options.length - 1
63+
this.setState({ index: this.state.index >= size ? 0 : this.state.index + 1 })
64+
}
65+
66+
function pressBackspaceKey () {
67+
// when backspace key is pressed and query is blank, delete the last tag
68+
if (!this.state.query.length) {
69+
this.deleteTag(this.props.tags.length - 1)
70+
}
71+
}
72+
73+
function filterSuggestions (query, suggestions) {
74+
const regexp = matchPartial(query)
75+
return suggestions.filter((item) => regexp.test(item.name))
76+
}
77+
2878
class ReactTags extends React.Component {
2979
constructor (props) {
3080
super(props)
3181

3282
this.state = {
3383
query: '',
3484
focused: false,
35-
expanded: false,
36-
selected: -1,
85+
options: [],
86+
index: -1,
3787
classNames: Object.assign({}, CLASS_NAMES, this.props.classNames)
3888
}
3989
}
@@ -51,55 +101,31 @@ class ReactTags extends React.Component {
51101
this.props.onInput(query)
52102
}
53103

54-
this.setState({ query })
104+
if (query !== this.state.query) {
105+
const filtered = filterSuggestions(query, this.props.suggestions)
106+
const options = filtered.slice(0, this.props.maxSuggestionsLength)
107+
108+
this.setState({ query, options })
109+
}
55110
}
56111

57112
onKeyDown (e) {
58-
const { query, selected } = this.state
59-
const { delimiters } = this.props
60-
61-
// when one of the terminating keys is pressed, add current query to the tags.
62-
if (delimiters.indexOf(e.key) > -1) {
63-
if (query || selected > -1) {
64-
e.preventDefault()
65-
}
66-
67-
if (query.length >= this.props.minQueryLength) {
68-
// Check if the user typed in an existing suggestion.
69-
const match = this.suggestions.state.options.findIndex((suggestion) => (
70-
suggestion.name.search(new RegExp(`^${query}$`, 'i')) === 0
71-
))
72-
73-
const index = selected === -1 ? match : selected
74-
75-
if (index > -1) {
76-
this.addTag(this.suggestions.state.options[index])
77-
} else if (this.props.allowNew) {
78-
this.addTag({ name: query })
79-
}
80-
}
113+
// when one of the terminating keys is pressed, add current query to the tags
114+
if (this.props.delimiters.indexOf(e.key) > -1) {
115+
pressDelimiterKey.call(this, e)
81116
}
82117

83118
// when backspace key is pressed and query is blank, delete the last tag
84-
if (e.key === KEYS.BACKSPACE && query.length === 0 && this.props.allowBackspace) {
85-
this.deleteTag(this.props.tags.length - 1)
119+
if (e.key === KEYS.BACKSPACE && this.props.allowBackspace) {
120+
pressBackspaceKey.call(this, e)
86121
}
87122

88123
if (e.key === KEYS.UP_ARROW) {
89-
e.preventDefault()
90-
91-
// if last item, cycle to the bottom
92-
if (selected <= 0) {
93-
this.setState({ selected: this.suggestions.state.options.length - 1 })
94-
} else {
95-
this.setState({ selected: selected - 1 })
96-
}
124+
pressUpKey.call(this, e)
97125
}
98126

99127
if (e.key === KEYS.DOWN_ARROW) {
100-
e.preventDefault()
101-
102-
this.setState({ selected: (selected + 1) % this.suggestions.state.options.length })
128+
pressDownKey.call(this, e)
103129
}
104130
}
105131

@@ -110,7 +136,7 @@ class ReactTags extends React.Component {
110136
}
111137

112138
onBlur () {
113-
this.setState({ focused: false, selected: -1 })
139+
this.setState({ focused: false, index: -1 })
114140

115141
if (this.props.onBlur) {
116142
this.props.onBlur()
@@ -135,7 +161,7 @@ class ReactTags extends React.Component {
135161
// reset the state
136162
this.setState({
137163
query: '',
138-
selected: -1
164+
index: -1
139165
})
140166
}
141167

@@ -183,9 +209,7 @@ class ReactTags extends React.Component {
183209
ref={(c) => { this.suggestions = c }}
184210
listboxId={listboxId}
185211
expanded={expanded}
186-
suggestions={this.props.suggestions}
187-
addTag={this.addTag.bind(this)}
188-
maxSuggestionsLength={this.props.maxSuggestionsLength} />
212+
addTag={this.addTag.bind(this)} />
189213
</div>
190214
</div>
191215
)

lib/Suggestions.js

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,28 @@
11
import React from 'react'
2+
import { matchAny } from './concerns/matchers'
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 = matchAny(query)
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

50-
if (this.props.selected === i) {
25+
if (this.props.index === i) {
5126
classNames.push(this.props.classNames.suggestionActive)
5227
}
5328

@@ -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/concerns/matchers.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
function escapeForRegExp (string) {
2+
return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&')
3+
}
4+
5+
export function matchAny (string) {
6+
return new RegExp(escapeForRegExp(string), 'gi')
7+
}
8+
9+
export function matchPartial (string) {
10+
return new RegExp(`(?:^|\\s)${escapeForRegExp(string)}`, 'i')
11+
}
12+
13+
export function matchExact (string) {
14+
return new RegExp(`^${escapeForRegExp(string)}$`, 'i')
15+
}

0 commit comments

Comments
 (0)