mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 09:46:22 +00:00
Tag suggest
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -98,44 +98,66 @@ iptFocusBorderColor = #369DCD
|
|||||||
&:hover
|
&:hover
|
||||||
background-color white
|
background-color white
|
||||||
.TagSelect
|
.TagSelect
|
||||||
white-space nowrap
|
.tags
|
||||||
overflow-x auto
|
white-space nowrap
|
||||||
position relative
|
overflow-x auto
|
||||||
margin-top 5px
|
position relative
|
||||||
noSelect()
|
max-width 350px
|
||||||
z-index 30
|
margin-top 5px
|
||||||
background-color #E6E6E6
|
noSelect()
|
||||||
.tagItem
|
z-index 30
|
||||||
background-color brandColor
|
background-color #E6E6E6
|
||||||
border-radius 2px
|
.tagItem
|
||||||
color white
|
background-color brandColor
|
||||||
margin 0 2px
|
border-radius 2px
|
||||||
padding 0
|
|
||||||
border 1px solid darken(brandColor, 10%)
|
|
||||||
button.tagRemoveBtn
|
|
||||||
color white
|
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-radius 2px
|
||||||
border none
|
border none
|
||||||
background-color transparent
|
|
||||||
padding 4px 2px
|
|
||||||
border-right 1px solid #E6E6E6
|
|
||||||
font-size 8px
|
|
||||||
line-height 12px
|
|
||||||
transition 0.1s
|
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
|
&:hover
|
||||||
background-color lighten(brandColor, 10%)
|
background-color darken(white, 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
|
|
||||||
.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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,16 +123,37 @@ 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)}>
|
||||||
{tagElements}
|
<div className='tags'>
|
||||||
<input
|
{tagElements}
|
||||||
type='text'
|
<input
|
||||||
onKeyDown={e => this.handleKeyDown(e)}
|
type='text'
|
||||||
ref='tagInput'
|
onKeyDown={e => this.handleKeyDown(e)}
|
||||||
valueLink={this.linkState('input')}
|
ref='tagInput'
|
||||||
placeholder='Click here to add tags'
|
valueLink={this.linkState('input')}
|
||||||
className='tagInput'/>
|
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>
|
</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
|
||||||
|
|||||||
Reference in New Issue
Block a user