diff --git a/browser/components/ColorPicker.js b/browser/components/ColorPicker.js new file mode 100644 index 00000000..9e0199c2 --- /dev/null +++ b/browser/components/ColorPicker.js @@ -0,0 +1,68 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { SketchPicker } from 'react-color' +import CSSModules from 'browser/lib/CSSModules' +import styles from './ColorPicker.styl' + +const componentHeight = 330 + +class ColorPicker extends React.Component { + constructor (props) { + super(props) + + this.state = { + color: this.props.color || '#939395' + } + + this.onColorChange = this.onColorChange.bind(this) + this.handleConfirm = this.handleConfirm.bind(this) + } + + componentWillReceiveProps (nextProps) { + this.onColorChange(nextProps.color) + } + + onColorChange (color) { + this.setState({ + color + }) + } + + handleConfirm () { + this.props.onConfirm(this.state.color) + } + + render () { + const { onReset, onCancel, targetRect } = this.props + const { color } = this.state + + const clientHeight = document.body.clientHeight + const alignX = targetRect.right + 4 + let alignY = targetRect.top + if (targetRect.top + componentHeight > clientHeight) { + alignY = targetRect.bottom - componentHeight + } + + return ( +
+
+ +
+ + + +
+
+ ) + } +} + +ColorPicker.propTypes = { + color: PropTypes.string, + targetRect: PropTypes.object, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + onReset: PropTypes.func.isRequired +} + +export default CSSModules(ColorPicker, styles) diff --git a/browser/components/ColorPicker.styl b/browser/components/ColorPicker.styl new file mode 100644 index 00000000..fbc1212a --- /dev/null +++ b/browser/components/ColorPicker.styl @@ -0,0 +1,39 @@ +.colorPicker + position fixed + z-index 2 + display flex + flex-direction column + +.cover + position fixed + top 0 + right 0 + bottom 0 + left 0 + +.footer + display flex + justify-content center + z-index 2 + align-items center + & > button + button + margin-left 10px + +.btn-cancel, +.btn-confirm, +.btn-reset + vertical-align middle + height 25px + margin-top 2.5px + border-radius 2px + border none + padding 0 5px + background-color $default-button-background + &:hover + background-color $default-button-background--hover +.btn-confirm + background-color #1EC38B + &:hover + background-color darken(#1EC38B, 25%) + + diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 2fc70a39..625bb38d 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types' import React from 'react' import { isArray } from 'lodash' +import invertColor from 'invert-color' import CSSModules from 'browser/lib/CSSModules' import { getTodoStatus } from 'browser/lib/getTodoStatus' import styles from './NoteItem.styl' @@ -13,29 +14,38 @@ import i18n from 'browser/lib/i18n' /** * @description Tag element component. * @param {string} tagName + * @param {string} color * @return {React.Component} */ -const TagElement = ({ tagName }) => ( - - #{tagName} - -) +const TagElement = ({ tagName, color }) => { + const style = {} + if (color) { + style.backgroundColor = color + style.color = invertColor(color, { black: '#222', white: '#f1f1f1', threshold: 0.3 }) + } + return ( + + #{tagName} + + ) +} /** * @description Tag element list component. * @param {Array|null} tags * @param {boolean} showTagsAlphabetically + * @param {Object} coloredTags * @return {React.Component} */ -const TagElementList = (tags, showTagsAlphabetically) => { +const TagElementList = (tags, showTagsAlphabetically, coloredTags) => { if (!isArray(tags)) { return [] } if (showTagsAlphabetically) { - return _.sortBy(tags).map(tag => TagElement({ tagName: tag })) + return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) } else { - return tags.map(tag => TagElement({ tagName: tag })) + return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) } } @@ -46,6 +56,7 @@ const TagElementList = (tags, showTagsAlphabetically) => { * @param {Function} handleNoteClick * @param {Function} handleNoteContextMenu * @param {Function} handleDragStart + * @param {Object} coloredTags * @param {string} dateDisplay */ const NoteItem = ({ @@ -59,7 +70,8 @@ const NoteItem = ({ storageName, folderName, viewType, - showTagsAlphabetically + showTagsAlphabetically, + coloredTags }) => (
{note.tags.length > 0 - ? TagElementList(note.tags, showTagsAlphabetically) + ? TagElementList(note.tags, showTagsAlphabetically, coloredTags) : ( +const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count, color}) => (
handleContextMenu(e, name)}> {isRelated ?
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index b401e634..11d8ac2a 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -792,6 +792,7 @@ class SnippetNoteDetail extends React.Component { showTagsAlphabetically={config.ui.showTagsAlphabetically} data={data} onChange={(e) => this.handleChange(e)} + coloredTags={config.coloredTags} />
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js index c5221f66..e3d9a567 100644 --- a/browser/main/Detail/TagSelect.js +++ b/browser/main/Detail/TagSelect.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types' import React from 'react' +import invertColor from 'invert-color' import CSSModules from 'browser/lib/CSSModules' import styles from './TagSelect.styl' import _ from 'lodash' @@ -185,19 +186,34 @@ class TagSelect extends React.Component { } render () { - const { value, className, showTagsAlphabetically } = this.props + const { value, className, showTagsAlphabetically, coloredTags } = this.props const tagList = _.isArray(value) ? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => { + const wrapperStyle = {} + const textStyle = {} + const BLACK = '#333333' + const WHITE = '#f1f1f1' + const color = coloredTags[tag] + const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE }) + let iconRemove = '../resources/icon/icon-x.svg' + if (color) { + wrapperStyle.backgroundColor = color + textStyle.color = invertedColor + } + if (invertedColor === WHITE) { + iconRemove = '../resources/icon/icon-x-light.svg' + } return ( - this.handleTagLabelClick(tag)}>#{tag} + this.handleTagLabelClick(tag)}>#{tag} ) @@ -246,7 +262,8 @@ TagSelect.contextTypes = { TagSelect.propTypes = { className: PropTypes.string, value: PropTypes.arrayOf(PropTypes.string), - onChange: PropTypes.func + onChange: PropTypes.func, + coloredTags: PropTypes.object } export default CSSModules(TagSelect, styles) diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index dbc9cfd3..ca513c04 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -1047,6 +1047,7 @@ class NoteList extends React.Component { storageName={this.getNoteStorage(note).name} viewType={viewType} showTagsAlphabetically={config.ui.showTagsAlphabetically} + coloredTags={config.coloredTags} /> ) } diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index b98d859d..640bedbf 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -20,6 +20,7 @@ import i18n from 'browser/lib/i18n' import context from 'browser/lib/context' import { remote } from 'electron' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' +import ColorPicker from 'browser/components/ColorPicker' function matchActiveTags (tags, activeTags) { return _.every(activeTags, v => tags.indexOf(v) >= 0) @@ -27,6 +28,22 @@ function matchActiveTags (tags, activeTags) { class SideNav extends React.Component { // TODO: should not use electron stuff v0.7 + constructor (props) { + super(props) + + this.state = { + colorPicker: { + show: false, + color: null, + tagName: null, + targetRect: null + } + } + + this.dismissColorPicker = this.dismissColorPicker.bind(this) + this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this) + this.handleColorPickerReset = this.handleColorPickerReset.bind(this) + } componentDidMount () { EventEmitter.on('side:preferences', this.handleMenuButtonClick) @@ -104,9 +121,64 @@ class SideNav extends React.Component { click: this.deleteTag.bind(this, tag) }) + menu.push({ + label: i18n.__('Customize Color'), + click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect()) + }) + context.popup(menu) } + dismissColorPicker () { + this.setState({ + colorPicker: { + show: false + } + }) + } + + displayColorPicker (tagName, rect) { + const { config } = this.props + this.setState({ + colorPicker: { + show: true, + color: config.coloredTags[tagName], + tagName, + targetRect: rect + } + }) + } + + handleColorPickerConfirm (color) { + const { dispatch, config: {coloredTags} } = this.props + const { colorPicker: { tagName } } = this.state + const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex}) + + const config = { coloredTags: newColoredTags } + ConfigManager.set(config) + dispatch({ + type: 'SET_CONFIG', + config + }) + this.dismissColorPicker() + } + + handleColorPickerReset () { + const { dispatch, config: {coloredTags} } = this.props + const { colorPicker: { tagName } } = this.state + const newColoredTags = Object.assign({}, coloredTags) + + delete newColoredTags[tagName] + + const config = { coloredTags: newColoredTags } + ConfigManager.set(config) + dispatch({ + type: 'SET_CONFIG', + config + }) + this.dismissColorPicker() + } + handleToggleButtonClick (e) { const { dispatch, config } = this.props @@ -207,6 +279,7 @@ class SideNav extends React.Component { tagListComponent () { const { data, location, config } = this.props + const { colorPicker } = this.state const activeTags = this.getActiveTags(location.pathname) const relatedTags = this.getRelatedTags(activeTags, data.noteMap) let tagList = _.sortBy(data.tagNoteMap.map( @@ -237,10 +310,11 @@ class SideNav extends React.Component { handleClickTagListItem={this.handleClickTagListItem.bind(this)} handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)} handleContextMenu={this.handleTagContextMenu.bind(this)} - isActive={this.getTagActive(location.pathname, tag.name)} + isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)} isRelated={tag.related} key={tag.name} count={tag.size} + color={config.coloredTags[tag.name]} /> ) }) @@ -333,6 +407,7 @@ class SideNav extends React.Component { render () { const { data, location, config, dispatch } = this.props + const { colorPicker: colorPickerState } = this.state const isFolded = config.isSideNavFolded @@ -349,6 +424,20 @@ class SideNav extends React.Component { useDragHandle /> }) + + let colorPicker + if (colorPickerState.show) { + colorPicker = ( + + ) + } + const style = {} if (!isFolded) style.width = this.props.width const isTagActive = location.pathname.match(/tag/) @@ -368,6 +457,7 @@ class SideNav extends React.Component {
{this.SideNavComponent(isFolded, storageList)} + {colorPicker}
) } diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 81165777..b8cfa1ef 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -86,7 +86,8 @@ export const DEFAULT_CONFIG = { token: '', username: '', password: '' - } + }, + coloredTags: {} } function validate (config) { diff --git a/browser/main/modals/PreferencesModal/FolderItem.styl b/browser/main/modals/PreferencesModal/FolderItem.styl index 2ded3ada..618e9bc4 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.styl +++ b/browser/main/modals/PreferencesModal/FolderItem.styl @@ -62,7 +62,7 @@ .folderItem-right-button vertical-align middle height 25px - margin-top 2.5px + margin-top 2px colorDefaultButton() border-radius 2px border $ui-border diff --git a/package.json b/package.json index 9838096f..95b4ecd7 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "i18n-2": "^0.7.2", "iconv-lite": "^0.4.19", "immutable": "^3.8.1", + "invert-color": "^2.0.0", "js-sequence-diagrams": "^1000000.0.6", "js-yaml": "^3.12.0", "katex": "^0.9.0", @@ -98,6 +99,7 @@ "react": "^15.5.4", "react-autosuggest": "^9.4.0", "react-codemirror": "^0.3.0", + "react-color": "^2.2.2", "react-debounce-render": "^4.0.1", "react-dom": "^15.0.2", "react-image-carousel": "^2.0.18", @@ -154,7 +156,6 @@ "merge-stream": "^1.0.0", "mock-require": "^3.0.1", "nib": "^1.1.0", - "react-color": "^2.2.2", "react-css-modules": "^3.7.6", "react-input-autosize": "^1.1.0", "react-router": "^2.4.0", diff --git a/resources/icon/icon-x-light.svg b/resources/icon/icon-x-light.svg new file mode 100644 index 00000000..39828f0b --- /dev/null +++ b/resources/icon/icon-x-light.svg @@ -0,0 +1,17 @@ + + + + x + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/components/TagListItem.snapshot.test.js b/tests/components/TagListItem.snapshot.test.js index 8bea2ccb..637844e6 100644 --- a/tests/components/TagListItem.snapshot.test.js +++ b/tests/components/TagListItem.snapshot.test.js @@ -3,7 +3,7 @@ import renderer from 'react-test-renderer' import TagListItem from 'browser/components/TagListItem' it('TagListItem renders correctly', () => { - const tagListItem = renderer.create() + const tagListItem = renderer.create() expect(tagListItem.toJSON()).toMatchSnapshot() }) diff --git a/tests/components/__snapshots__/TagListItem.snapshot.test.js.snap b/tests/components/__snapshots__/TagListItem.snapshot.test.js.snap index ad883222..e3798130 100644 --- a/tests/components/__snapshots__/TagListItem.snapshot.test.js.snap +++ b/tests/components/__snapshots__/TagListItem.snapshot.test.js.snap @@ -12,6 +12,14 @@ exports[`TagListItem renders correctly 1`] = ` className="tagList-item" onClick={[Function]} > +