Skip to content

Commit 409eaf5

Browse files
committed
Tag suggest
1 parent 7e04fd3 commit 409eaf5

5 files changed

Lines changed: 170 additions & 57 deletions

File tree

browser/main/HomePage.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class HomePage extends React.Component {
9999
}
100100

101101
render () {
102-
let { dispatch, status, articles, allArticles, activeArticle, folders, filters } = this.props
102+
let { dispatch, status, articles, allArticles, activeArticle, folders, tags, filters } = this.props
103103

104104
return (
105105
<div className='HomePage'>
@@ -129,6 +129,7 @@ class HomePage extends React.Component {
129129
activeArticle={activeArticle}
130130
folders={folders}
131131
status={status}
132+
tags={tags}
132133
filters={filters}
133134
/>
134135
</div>
@@ -164,6 +165,11 @@ function remap (state) {
164165
})
165166
let allArticles = articles.slice()
166167

168+
let tags = _.uniq(allArticles.reduce((sum, article) => {
169+
if (!_.isArray(article.tags)) return sum
170+
return sum.concat(article.tags)
171+
}, []))
172+
167173
// Filter articles
168174
let filters = status.search.split(' ')
169175
.map(key => key.trim())
@@ -254,6 +260,7 @@ function remap (state) {
254260
allArticles,
255261
articles,
256262
activeArticle,
263+
tags,
257264
filters: {
258265
folder: folderFilters,
259266
tag: tagFilters,

browser/main/HomePage/ArticleDetail.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export default class ArticleDetail extends React.Component {
303303
}
304304

305305
renderEdit () {
306-
let { folders, status } = this.props
306+
let { folders, status, tags } = this.props
307307

308308
let folderOptions = folders.map(folder => {
309309
return (
@@ -326,6 +326,7 @@ export default class ArticleDetail extends React.Component {
326326
<TagSelect
327327
tags={this.state.article.tags}
328328
onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
329+
suggestTags={tags}
329330
/>
330331

331332
{status.isTutorialOpen ? tagSelectTutorialElement : null}

browser/styles/main/HomeContainer/components/ArticleDetail.styl

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -98,44 +98,66 @@ iptFocusBorderColor = #369DCD
9898
&:hover
9999
background-color white
100100
.TagSelect
101-
white-space nowrap
102-
overflow-x auto
103-
position relative
104-
margin-top 5px
105-
noSelect()
106-
z-index 30
107-
background-color #E6E6E6
108-
.tagItem
109-
background-color brandColor
110-
border-radius 2px
111-
color white
112-
margin 0 2px
113-
padding 0
114-
border 1px solid darken(brandColor, 10%)
115-
button.tagRemoveBtn
101+
.tags
102+
white-space nowrap
103+
overflow-x auto
104+
position relative
105+
max-width 350px
106+
margin-top 5px
107+
noSelect()
108+
z-index 30
109+
background-color #E6E6E6
110+
.tagItem
111+
background-color brandColor
112+
border-radius 2px
116113
color white
114+
margin 0 2px
115+
padding 0
116+
border 1px solid darken(brandColor, 10%)
117+
button.tagRemoveBtn
118+
color white
119+
border-radius 2px
120+
border none
121+
background-color transparent
122+
padding 4px 2px
123+
border-right 1px solid #E6E6E6
124+
font-size 8px
125+
line-height 12px
126+
transition 0.1s
127+
&:hover
128+
background-color lighten(brandColor, 10%)
129+
.tagLabel
130+
padding 4px 4px
131+
font-size 12px
132+
line-height 12px
133+
input.tagInput
134+
background-color transparent
135+
outline none
136+
margin 0 2px
117137
border-radius 2px
118138
border none
119-
background-color transparent
120-
padding 4px 2px
121-
border-right 1px solid #E6E6E6
122-
font-size 8px
123-
line-height 12px
124139
transition 0.1s
140+
height 18px
141+
.suggestTags
142+
position fixed
143+
width 150px
144+
max-height 150px
145+
background-color white
146+
z-index 5
147+
border 1px solid borderColor
148+
border-radius 5px
149+
button
150+
width 100%
151+
display block
152+
padding 0 15px
153+
height 33px
154+
line-height 33px
155+
background-color transparent
156+
border none
157+
text-align left
158+
font-size 14px
125159
&:hover
126-
background-color lighten(brandColor, 10%)
127-
.tagLabel
128-
padding 4px 4px
129-
font-size 12px
130-
line-height 12px
131-
input.tagInput
132-
background-color transparent
133-
outline none
134-
margin 0 2px
135-
border-radius 2px
136-
border none
137-
transition 0.1s
138-
height 18px
160+
background-color darken(white, 10%)
139161
.right
140162
button
141163
cursor pointer
@@ -222,9 +244,6 @@ iptFocusBorderColor = #369DCD
222244
display inline-block
223245
&:hover
224246
background-color darken(white, 10%)
225-
226-
227-
228247
.title
229248
absolute left top bottom
230249
right 150px

lib/components/ModeSelect.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default class ModeSelect extends React.Component {
1818
}
1919
}
2020

21-
componentDidMount (e) {
21+
componentDidMount () {
2222
this.blurHandler = e => {
2323
let searchElement = ReactDOM.findDOMNode(this.refs.search)
2424
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
@@ -28,7 +28,7 @@ export default class ModeSelect extends React.Component {
2828
window.addEventListener('click', this.blurHandler)
2929
}
3030

31-
componentWillUnmount (e) {
31+
componentWillUnmount () {
3232
window.removeEventListener('click', this.blurHandler)
3333
let searchElement = ReactDOM.findDOMNode(this.refs.search)
3434
if (searchElement != null && this.searchKeyDownListener != null) {

lib/components/TagSelect.js

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,54 @@ import ReactDOM from 'react-dom'
33
import _ from 'lodash'
44
import linkState from 'boost/linkState'
55

6+
function isNotEmptyString (str) {
7+
return _.isString(str) && str.length > 0
8+
}
9+
610
export default class TagSelect extends React.Component {
711
constructor (props) {
812
super(props)
913

1014
this.state = {
11-
input: ''
15+
input: '',
16+
isInputFocused: false
1217
}
1318
}
1419

15-
handleKeyDown (e) {
16-
if (e.keyCode !== 13) return false
17-
e.preventDefault()
20+
componentDidMount () {
21+
this.blurInputBlurHandler = e => {
22+
if (ReactDOM.findDOMNode(this.refs.tagInput) !== document.activeElement) {
23+
this.setState({isInputFocused: false})
24+
}
25+
}
26+
window.addEventListener('click', this.blurInputBlurHandler)
27+
}
28+
29+
componentWillUnmount (e) {
30+
window.removeEventListener('click', this.blurInputBlurHandler)
31+
}
1832

33+
// Suggestは必ずInputの下に位置するようにする
34+
componentDidUpdate () {
35+
if (this.shouldShowSuggest()) {
36+
let inputRect = ReactDOM.findDOMNode(this.refs.tagInput).getBoundingClientRect()
37+
let suggestElement = ReactDOM.findDOMNode(this.refs.suggestTags)
38+
if (suggestElement != null) {
39+
suggestElement.style.top = inputRect.top + 20 + 'px'
40+
suggestElement.style.left = inputRect.left + 'px'
41+
}
42+
}
43+
}
44+
45+
shouldShowSuggest () {
46+
return this.state.isInputFocused && isNotEmptyString(this.state.input)
47+
}
48+
49+
addTag (tag, clearInput = true) {
1950
let tags = this.props.tags.slice(0)
20-
let newTag = this.state.input.trim()
51+
let newTag = tag.trim()
2152

22-
if (newTag.length === 0) {
53+
if (newTag.length === 0 && clearInput) {
2354
this.setState({input: ''})
2455
return
2556
}
@@ -30,13 +61,38 @@ export default class TagSelect extends React.Component {
3061
if (_.isFunction(this.props.onChange)) {
3162
this.props.onChange(newTag, tags)
3263
}
33-
this.setState({input: ''})
64+
if (clearInput) this.setState({input: ''})
65+
}
66+
67+
handleKeyDown (e) {
68+
switch (e.keyCode) {
69+
case 8:
70+
{
71+
if (this.state.input.length > 0) break
72+
e.preventDefault()
73+
74+
let tags = this.props.tags.slice(0)
75+
tags.pop()
76+
77+
this.props.onChange(null, tags)
78+
}
79+
break
80+
case 13:
81+
{
82+
e.preventDefault()
83+
this.addTag(this.state.input)
84+
}
85+
}
3486
}
3587

3688
handleThisClick (e) {
3789
ReactDOM.findDOMNode(this.refs.tagInput).focus()
3890
}
3991

92+
handleInputFocus (e) {
93+
this.setState({isInputFocused: true})
94+
}
95+
4096
handleItemRemoveButton (tag) {
4197
return e => {
4298
e.stopPropagation()
@@ -50,33 +106,63 @@ export default class TagSelect extends React.Component {
50106
}
51107
}
52108

109+
handleSuggestClick (tag) {
110+
return e => {
111+
this.addTag(tag)
112+
}
113+
}
114+
53115
render () {
54-
var tagElements = _.isArray(this.props.tags)
116+
let { tags, suggestTags } = this.props
117+
118+
let tagElements = _.isArray(tags)
55119
? this.props.tags.map(tag => (
56120
<span key={tag} className='tagItem'>
57121
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button>
58122
<span className='tagLabel'>{tag}</span>
59123
</span>))
60124
: null
61125

126+
let suggestElements = this.shouldShowSuggest() ? suggestTags
127+
.filter(tag => {
128+
return tag.match(this.state.input)
129+
})
130+
.map(tag => {
131+
return <button onClick={e => this.handleSuggestClick(tag)(e)} key={tag}>{tag}</button>
132+
})
133+
: null
134+
62135
return (
63136
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
64-
{tagElements}
65-
<input
66-
type='text'
67-
onKeyDown={e => this.handleKeyDown(e)}
68-
ref='tagInput'
69-
valueLink={this.linkState('input')}
70-
placeholder='Click here to add tags'
71-
className='tagInput'/>
137+
<div className='tags'>
138+
{tagElements}
139+
<input
140+
type='text'
141+
onKeyDown={e => this.handleKeyDown(e)}
142+
ref='tagInput'
143+
valueLink={this.linkState('input')}
144+
placeholder='Click here to add tags'
145+
className='tagInput'
146+
onFocus={e => this.handleInputFocus(e)}
147+
/>
148+
</div>
149+
{suggestElements != null && suggestElements.length > 0
150+
? (
151+
<div ref='suggestTags' className='suggestTags'>
152+
{suggestElements}
153+
</div>
154+
)
155+
: null
156+
}
72157
</div>
73158
)
74159
}
75160
}
76161

77162
TagSelect.propTypes = {
78163
tags: PropTypes.arrayOf(PropTypes.string),
79-
onChange: PropTypes.func
164+
onChange: PropTypes.func,
165+
suggestTags: PropTypes.array
80166
}
81167

82168
TagSelect.prototype.linkState = linkState

0 commit comments

Comments
 (0)