diff --git a/browser/main/HomePage.js b/browser/main/HomePage.js index beb312ee..c40b4dfb 100644 --- a/browser/main/HomePage.js +++ b/browser/main/HomePage.js @@ -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 (
@@ -129,6 +129,7 @@ class HomePage extends React.Component { activeArticle={activeArticle} folders={folders} status={status} + tags={tags} filters={filters} />
@@ -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, diff --git a/browser/main/HomePage/ArticleDetail.js b/browser/main/HomePage/ArticleDetail.js index 10c702c5..6ae52e7d 100644 --- a/browser/main/HomePage/ArticleDetail.js +++ b/browser/main/HomePage/ArticleDetail.js @@ -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 { this.handleTagsChange(tags, tag)} + suggestTags={tags} /> {status.isTutorialOpen ? tagSelectTutorialElement : null} diff --git a/browser/styles/main/HomeContainer/components/ArticleDetail.styl b/browser/styles/main/HomeContainer/components/ArticleDetail.styl index 0a3182f4..2f4ae10c 100644 --- a/browser/styles/main/HomeContainer/components/ArticleDetail.styl +++ b/browser/styles/main/HomeContainer/components/ArticleDetail.styl @@ -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 diff --git a/lib/components/ModeSelect.js b/lib/components/ModeSelect.js index 554e361c..28404d20 100644 --- a/lib/components/ModeSelect.js +++ b/lib/components/ModeSelect.js @@ -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) { diff --git a/lib/components/TagSelect.js b/lib/components/TagSelect.js index b7fbead8..f000d32d 100644 --- a/lib/components/TagSelect.js +++ b/lib/components/TagSelect.js @@ -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 => ( @@ -59,16 +123,37 @@ export default class TagSelect extends React.Component { )) : null + let suggestElements = this.shouldShowSuggest() ? suggestTags + .filter(tag => { + return tag.match(this.state.input) + }) + .map(tag => { + return + }) + : null + return (
this.handleThisClick(e)}> - {tagElements} - this.handleKeyDown(e)} - ref='tagInput' - valueLink={this.linkState('input')} - placeholder='Click here to add tags' - className='tagInput'/> +
+ {tagElements} + this.handleKeyDown(e)} + ref='tagInput' + valueLink={this.linkState('input')} + placeholder='Click here to add tags' + className='tagInput' + onFocus={e => this.handleInputFocus(e)} + /> +
+ {suggestElements != null && suggestElements.length > 0 + ? ( +
+ {suggestElements} +
+ ) + : null + }
) } @@ -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