1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 09:46:22 +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 () {
let { dispatch, status, articles, allArticles, activeArticle, folders, filters } = this.props
let { dispatch, status, articles, allArticles, activeArticle, folders, tags, filters } = this.props
return (
<div className='HomePage'>
@@ -129,6 +129,7 @@ class HomePage extends React.Component {
activeArticle={activeArticle}
folders={folders}
status={status}
tags={tags}
filters={filters}
/>
</div>
@@ -164,6 +165,11 @@ function remap (state) {
})
let allArticles = articles.slice()
let tags = _.uniq(allArticles.reduce((sum, article) => {
if (!_.isArray(article.tags)) return sum
return sum.concat(article.tags)
}, []))
// Filter articles
let filters = status.search.split(' ')
.map(key => key.trim())
@@ -254,6 +260,7 @@ function remap (state) {
allArticles,
articles,
activeArticle,
tags,
filters: {
folder: folderFilters,
tag: tagFilters,

View File

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

View File

@@ -98,44 +98,66 @@ iptFocusBorderColor = #369DCD
&:hover
background-color white
.TagSelect
white-space nowrap
overflow-x auto
position relative
margin-top 5px
noSelect()
z-index 30
background-color #E6E6E6
.tagItem
background-color brandColor
border-radius 2px
color white
margin 0 2px
padding 0
border 1px solid darken(brandColor, 10%)
button.tagRemoveBtn
.tags
white-space nowrap
overflow-x auto
position relative
max-width 350px
margin-top 5px
noSelect()
z-index 30
background-color #E6E6E6
.tagItem
background-color brandColor
border-radius 2px
color white
margin 0 2px
padding 0
border 1px solid darken(brandColor, 10%)
button.tagRemoveBtn
color white
border-radius 2px
border none
background-color transparent
padding 4px 2px
border-right 1px solid #E6E6E6
font-size 8px
line-height 12px
transition 0.1s
&:hover
background-color lighten(brandColor, 10%)
.tagLabel
padding 4px 4px
font-size 12px
line-height 12px
input.tagInput
background-color transparent
outline none
margin 0 2px
border-radius 2px
border none
background-color transparent
padding 4px 2px
border-right 1px solid #E6E6E6
font-size 8px
line-height 12px
transition 0.1s
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 lighten(brandColor, 10%)
.tagLabel
padding 4px 4px
font-size 12px
line-height 12px
input.tagInput
background-color transparent
outline none
margin 0 2px
border-radius 2px
border none
transition 0.1s
height 18px
background-color darken(white, 10%)
.right
button
cursor pointer
@@ -222,9 +244,6 @@ iptFocusBorderColor = #369DCD
display inline-block
&:hover
background-color darken(white, 10%)
.title
absolute left top bottom
right 150px

View File

@@ -18,7 +18,7 @@ export default class ModeSelect extends React.Component {
}
}
componentDidMount (e) {
componentDidMount () {
this.blurHandler = e => {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
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)
}
componentWillUnmount (e) {
componentWillUnmount () {
window.removeEventListener('click', this.blurHandler)
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (searchElement != null && this.searchKeyDownListener != null) {

View File

@@ -3,23 +3,54 @@ import ReactDOM from 'react-dom'
import _ from 'lodash'
import linkState from 'boost/linkState'
function isNotEmptyString (str) {
return _.isString(str) && str.length > 0
}
export default class TagSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
input: ''
input: '',
isInputFocused: false
}
}
handleKeyDown (e) {
if (e.keyCode !== 13) return false
e.preventDefault()
componentDidMount () {
this.blurInputBlurHandler = e => {
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 newTag = this.state.input.trim()
let newTag = tag.trim()
if (newTag.length === 0) {
if (newTag.length === 0 && clearInput) {
this.setState({input: ''})
return
}
@@ -30,13 +61,38 @@ export default class TagSelect extends React.Component {
if (_.isFunction(this.props.onChange)) {
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) {
ReactDOM.findDOMNode(this.refs.tagInput).focus()
}
handleInputFocus (e) {
this.setState({isInputFocused: true})
}
handleItemRemoveButton (tag) {
return e => {
e.stopPropagation()
@@ -50,8 +106,16 @@ export default class TagSelect extends React.Component {
}
}
handleSuggestClick (tag) {
return e => {
this.addTag(tag)
}
}
render () {
var tagElements = _.isArray(this.props.tags)
let { tags, suggestTags } = this.props
let tagElements = _.isArray(tags)
? this.props.tags.map(tag => (
<span key={tag} className='tagItem'>
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button>
@@ -59,16 +123,37 @@ export default class TagSelect extends React.Component {
</span>))
: 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 (
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
{tagElements}
<input
type='text'
onKeyDown={e => this.handleKeyDown(e)}
ref='tagInput'
valueLink={this.linkState('input')}
placeholder='Click here to add tags'
className='tagInput'/>
<div className='tags'>
{tagElements}
<input
type='text'
onKeyDown={e => this.handleKeyDown(e)}
ref='tagInput'
valueLink={this.linkState('input')}
placeholder='Click here to add tags'
className='tagInput'
onFocus={e => this.handleInputFocus(e)}
/>
</div>
{suggestElements != null && suggestElements.length > 0
? (
<div ref='suggestTags' className='suggestTags'>
{suggestElements}
</div>
)
: null
}
</div>
)
}
@@ -76,7 +161,8 @@ export default class TagSelect extends React.Component {
TagSelect.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func
onChange: PropTypes.func,
suggestTags: PropTypes.array
}
TagSelect.prototype.linkState = linkState