1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 17:56:25 +00:00

Tag suggest

This commit is contained in:
Rokt33r
2015-11-16 04:06:14 +09:00
parent 7e04fd342c
commit 409eaf54c1
5 changed files with 170 additions and 57 deletions

View File

@@ -99,7 +99,7 @@ class HomePage extends React.Component {
} }
render () { render () {
let { dispatch, status, articles, allArticles, activeArticle, folders, filters } = this.props let { dispatch, status, articles, allArticles, activeArticle, folders, tags, filters } = this.props
return ( return (
<div className='HomePage'> <div className='HomePage'>
@@ -129,6 +129,7 @@ class HomePage extends React.Component {
activeArticle={activeArticle} activeArticle={activeArticle}
folders={folders} folders={folders}
status={status} status={status}
tags={tags}
filters={filters} filters={filters}
/> />
</div> </div>
@@ -164,6 +165,11 @@ function remap (state) {
}) })
let allArticles = articles.slice() let allArticles = articles.slice()
let tags = _.uniq(allArticles.reduce((sum, article) => {
if (!_.isArray(article.tags)) return sum
return sum.concat(article.tags)
}, []))
// Filter articles // Filter articles
let filters = status.search.split(' ') let filters = status.search.split(' ')
.map(key => key.trim()) .map(key => key.trim())
@@ -254,6 +260,7 @@ function remap (state) {
allArticles, allArticles,
articles, articles,
activeArticle, activeArticle,
tags,
filters: { filters: {
folder: folderFilters, folder: folderFilters,
tag: tagFilters, tag: tagFilters,

View File

@@ -303,7 +303,7 @@ export default class ArticleDetail extends React.Component {
} }
renderEdit () { renderEdit () {
let { folders, status } = this.props let { folders, status, tags } = this.props
let folderOptions = folders.map(folder => { let folderOptions = folders.map(folder => {
return ( return (
@@ -326,6 +326,7 @@ export default class ArticleDetail extends React.Component {
<TagSelect <TagSelect
tags={this.state.article.tags} tags={this.state.article.tags}
onChange={(tags, tag) => this.handleTagsChange(tags, tag)} onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
suggestTags={tags}
/> />
{status.isTutorialOpen ? tagSelectTutorialElement : null} {status.isTutorialOpen ? tagSelectTutorialElement : null}

View File

@@ -98,9 +98,11 @@ iptFocusBorderColor = #369DCD
&:hover &:hover
background-color white background-color white
.TagSelect .TagSelect
.tags
white-space nowrap white-space nowrap
overflow-x auto overflow-x auto
position relative position relative
max-width 350px
margin-top 5px margin-top 5px
noSelect() noSelect()
z-index 30 z-index 30
@@ -136,6 +138,26 @@ iptFocusBorderColor = #369DCD
border none border none
transition 0.1s transition 0.1s
height 18px height 18px
.suggestTags
position fixed
width 150px
max-height 150px
background-color white
z-index 5
border 1px solid borderColor
border-radius 5px
button
width 100%
display block
padding 0 15px
height 33px
line-height 33px
background-color transparent
border none
text-align left
font-size 14px
&:hover
background-color darken(white, 10%)
.right .right
button button
cursor pointer cursor pointer
@@ -222,9 +244,6 @@ iptFocusBorderColor = #369DCD
display inline-block display inline-block
&:hover &:hover
background-color darken(white, 10%) background-color darken(white, 10%)
.title .title
absolute left top bottom absolute left top bottom
right 150px right 150px

View File

@@ -18,7 +18,7 @@ export default class ModeSelect extends React.Component {
} }
} }
componentDidMount (e) { componentDidMount () {
this.blurHandler = e => { this.blurHandler = e => {
let searchElement = ReactDOM.findDOMNode(this.refs.search) let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) { if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
@@ -28,7 +28,7 @@ export default class ModeSelect extends React.Component {
window.addEventListener('click', this.blurHandler) window.addEventListener('click', this.blurHandler)
} }
componentWillUnmount (e) { componentWillUnmount () {
window.removeEventListener('click', this.blurHandler) window.removeEventListener('click', this.blurHandler)
let searchElement = ReactDOM.findDOMNode(this.refs.search) let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (searchElement != null && this.searchKeyDownListener != null) { if (searchElement != null && this.searchKeyDownListener != null) {

View File

@@ -3,23 +3,54 @@ import ReactDOM from 'react-dom'
import _ from 'lodash' import _ from 'lodash'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
function isNotEmptyString (str) {
return _.isString(str) && str.length > 0
}
export default class TagSelect extends React.Component { export default class TagSelect extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
input: '' input: '',
isInputFocused: false
} }
} }
handleKeyDown (e) { componentDidMount () {
if (e.keyCode !== 13) return false this.blurInputBlurHandler = e => {
e.preventDefault() if (ReactDOM.findDOMNode(this.refs.tagInput) !== document.activeElement) {
this.setState({isInputFocused: false})
}
}
window.addEventListener('click', this.blurInputBlurHandler)
}
componentWillUnmount (e) {
window.removeEventListener('click', this.blurInputBlurHandler)
}
// Suggestは必ずInputの下に位置するようにする
componentDidUpdate () {
if (this.shouldShowSuggest()) {
let inputRect = ReactDOM.findDOMNode(this.refs.tagInput).getBoundingClientRect()
let suggestElement = ReactDOM.findDOMNode(this.refs.suggestTags)
if (suggestElement != null) {
suggestElement.style.top = inputRect.top + 20 + 'px'
suggestElement.style.left = inputRect.left + 'px'
}
}
}
shouldShowSuggest () {
return this.state.isInputFocused && isNotEmptyString(this.state.input)
}
addTag (tag, clearInput = true) {
let tags = this.props.tags.slice(0) let tags = this.props.tags.slice(0)
let newTag = this.state.input.trim() let newTag = tag.trim()
if (newTag.length === 0) { if (newTag.length === 0 && clearInput) {
this.setState({input: ''}) this.setState({input: ''})
return return
} }
@@ -30,13 +61,38 @@ export default class TagSelect extends React.Component {
if (_.isFunction(this.props.onChange)) { if (_.isFunction(this.props.onChange)) {
this.props.onChange(newTag, tags) this.props.onChange(newTag, tags)
} }
this.setState({input: ''}) if (clearInput) this.setState({input: ''})
}
handleKeyDown (e) {
switch (e.keyCode) {
case 8:
{
if (this.state.input.length > 0) break
e.preventDefault()
let tags = this.props.tags.slice(0)
tags.pop()
this.props.onChange(null, tags)
}
break
case 13:
{
e.preventDefault()
this.addTag(this.state.input)
}
}
} }
handleThisClick (e) { handleThisClick (e) {
ReactDOM.findDOMNode(this.refs.tagInput).focus() ReactDOM.findDOMNode(this.refs.tagInput).focus()
} }
handleInputFocus (e) {
this.setState({isInputFocused: true})
}
handleItemRemoveButton (tag) { handleItemRemoveButton (tag) {
return e => { return e => {
e.stopPropagation() e.stopPropagation()
@@ -50,8 +106,16 @@ export default class TagSelect extends React.Component {
} }
} }
handleSuggestClick (tag) {
return e => {
this.addTag(tag)
}
}
render () { render () {
var tagElements = _.isArray(this.props.tags) let { tags, suggestTags } = this.props
let tagElements = _.isArray(tags)
? this.props.tags.map(tag => ( ? this.props.tags.map(tag => (
<span key={tag} className='tagItem'> <span key={tag} className='tagItem'>
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button> <button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button>
@@ -59,8 +123,18 @@ export default class TagSelect extends React.Component {
</span>)) </span>))
: null : null
let suggestElements = this.shouldShowSuggest() ? suggestTags
.filter(tag => {
return tag.match(this.state.input)
})
.map(tag => {
return <button onClick={e => this.handleSuggestClick(tag)(e)} key={tag}>{tag}</button>
})
: null
return ( return (
<div className='TagSelect' onClick={e => this.handleThisClick(e)}> <div className='TagSelect' onClick={e => this.handleThisClick(e)}>
<div className='tags'>
{tagElements} {tagElements}
<input <input
type='text' type='text'
@@ -68,7 +142,18 @@ export default class TagSelect extends React.Component {
ref='tagInput' ref='tagInput'
valueLink={this.linkState('input')} valueLink={this.linkState('input')}
placeholder='Click here to add tags' placeholder='Click here to add tags'
className='tagInput'/> className='tagInput'
onFocus={e => this.handleInputFocus(e)}
/>
</div>
{suggestElements != null && suggestElements.length > 0
? (
<div ref='suggestTags' className='suggestTags'>
{suggestElements}
</div>
)
: null
}
</div> </div>
) )
} }
@@ -76,7 +161,8 @@ export default class TagSelect extends React.Component {
TagSelect.propTypes = { TagSelect.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string), tags: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func onChange: PropTypes.func,
suggestTags: PropTypes.array
} }
TagSelect.prototype.linkState = linkState TagSelect.prototype.linkState = linkState