import React, { PropTypes } from 'react' import ReactDOM from 'react-dom' import moment from 'moment' import _ from 'lodash' import ModeIcon from 'boost/components/ModeIcon' import MarkdownPreview from 'boost/components/MarkdownPreview' import CodeEditor from 'boost/components/CodeEditor' import { IDLE_MODE, EDIT_MODE, switchMode, switchArticle, switchFolder, clearSearch, lockStatus, unlockStatus, updateArticle, destroyArticle, NEW } from 'boost/actions' import linkState from 'boost/linkState' import FolderMark from 'boost/components/FolderMark' import TagLink from 'boost/components/TagLink' import TagSelect from 'boost/components/TagSelect' import ModeSelect from 'boost/components/ModeSelect' import activityRecord from 'boost/activityRecord' import api from 'boost/api' import ShareButton from './ShareButton' const electron = require('electron') const clipboard = electron.clipboard const BRAND_COLOR = '#18AF90' const editDeleteTutorialElement = ( Edit / Delete a post press `e`/`d` ) const tagSelectTutorialElement = ( Attach some tags here! ) const modeSelectTutorialElement = ( Select code syntax!! ) function notify (...args) { return new window.Notification(...args) } function makeInstantArticle (article) { return Object.assign({}, article) } export default class ArticleDetail extends React.Component { constructor (props) { super(props) this.state = { article: makeInstantArticle(props.activeArticle), previewMode: false, isArticleEdited: false, isTagChanged: false, isTitleChanged: false, isContentChanged: false, isModeChanged: false, openShareDropdown: false } } componentDidMount () { this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000) this.shareDropdownInterceptor = e => { e.stopPropagation() } } componentWillUnmount () { clearInterval(this.refreshTimer) } componentDidUpdate (prevProps) { let isModeChanged = prevProps.status.mode !== this.props.status.mode if (isModeChanged && this.props.status.mode === EDIT_MODE) { ReactDOM.findDOMNode(this.refs.title).focus() } } componentWillReceiveProps (nextProps) { let nextState = {} let isArticleChanged = nextProps.activeArticle != null && (nextProps.activeArticle.key !== this.state.article.key) let isModeChanged = nextProps.status.mode !== this.props.status.mode // Reset article input if (isArticleChanged || (isModeChanged && nextProps.status.mode !== IDLE_MODE)) { Object.assign(nextState, { article: makeInstantArticle(nextProps.activeArticle) }) } // Clean state if (isModeChanged) { Object.assign(nextState, { openDeleteConfirmMenu: false, previewMode: false, isArticleEdited: false, isTagChanged: false, isTitleChanged: false, isContentChanged: false }) } this.setState(nextState) } renderEmpty () { return (
Command(⌘) + Enter to create a new post
) } handleClipboardButtonClick (e) { activityRecord.emit('MAIN_DETAIL_COPY') clipboard.writeText(this.props.activeArticle.content) notify('Saved to Clipboard!', { body: 'Paste it wherever you want!' }) } handleEditButtonClick (e) { let { dispatch } = this.props dispatch(switchMode(EDIT_MODE)) } handleDeleteButtonClick (e) { this.setState({openDeleteConfirmMenu: true}) } handleDeleteConfirmButtonClick (e) { let { dispatch, activeArticle } = this.props dispatch(destroyArticle(activeArticle.key)) activityRecord.emit('ARTICLE_DESTROY') this.setState({openDeleteConfirmMenu: false}) } handleDeleteCancelButtonClick (e) { this.setState({openDeleteConfirmMenu: false}) } renderIdle () { let { status, activeArticle, folders, user } = this.props let tags = activeArticle.tags != null ? activeArticle.tags.length > 0 ? activeArticle.tags.map(tag => { return () }) : ( Not tagged yet ) : null let folder = _.findWhere(folders, {key: activeArticle.FolderKey}) let title = activeArticle.title.trim().length === 0 ? (Untitled) : activeArticle.title return (
{this.state.openDeleteConfirmMenu ? (
Are you sure to delete this article?
) : (
{folder.name}  Created : {moment(activeArticle.createdAt).format('YYYY/MM/DD')}  Updated : {moment(activeArticle.updatedAt).format('YYYY/MM/DD')}
{tags}
{status.isTutorialOpen ? editDeleteTutorialElement : null}
) }
{title}
{activeArticle.mode === 'markdown' ? : this.handleContentChange(e, value)} mode={activeArticle.mode} code={activeArticle.content}/> }
) } handleCancelButtonClick (e) { let { activeArticle, dispatch } = this.props if (activeArticle.status === NEW) { dispatch(switchArticle(null)) } dispatch(switchMode(IDLE_MODE)) } handleSaveButtonClick (e) { let { dispatch, folders, status } = this.props let article = this.state.article let newArticle = Object.assign({}, article) let folder = _.findWhere(folders, {key: article.FolderKey}) if (folder == null) return false dispatch(unlockStatus()) newArticle.status = null newArticle.updatedAt = new Date() newArticle.title = newArticle.title.trim() if (newArticle.createdAt == null) { newArticle.createdAt = new Date() if (newArticle.title.length === 0) { newArticle.title = `Created at ${moment(newArticle.createdAt).format('YYYY/MM/DD HH:mm')}` } activityRecord.emit('ARTICLE_CREATE', {mode: newArticle.mode}) } else { activityRecord.emit('ARTICLE_UPDATE', {mode: newArticle.mode}) } dispatch(updateArticle(newArticle)) dispatch(switchMode(IDLE_MODE)) // Folder filterがかかっている時に、 // Searchを初期化し、更新先のFolder filterをかける // かかれていない時に // Searchを初期化する if (status.targetFolders.length > 0) dispatch(switchFolder(folder.name)) else dispatch(clearSearch()) dispatch(switchArticle(newArticle.key)) } handleFolderKeyChange (e) { let article = this.state.article article.FolderKey = e.target.value this.setState({article: article}) } handleTitleChange (e) { let { article } = this.state article.title = e.target.value let _isTitleChanged = article.title !== this.props.activeArticle.title let { isTagChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state let _isArticleEdited = _isTitleChanged || isTagChanged || isContentChanged || isModeChanged this.setState({ article, isTitleChanged: _isTitleChanged, isArticleEdited: _isArticleEdited }, () => { if (isArticleEdited !== _isArticleEdited) { let { dispatch } = this.props if (_isArticleEdited) { console.log('lockit') dispatch(lockStatus()) } else { console.log('unlockit') dispatch(unlockStatus()) } } }) } handleTagsChange (newTag, tags) { let article = this.state.article article.tags = tags let _isTagChanged = _.difference(article.tags, this.props.activeArticle.tags).length > 0 || _.difference(this.props.activeArticle.tags, article.tags).length > 0 let { isTitleChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state let _isArticleEdited = _isTagChanged || isTitleChanged || isContentChanged || isModeChanged this.setState({ article, isTagChanged: _isTagChanged, isArticleEdited: _isArticleEdited }, () => { if (isArticleEdited !== _isArticleEdited) { let { dispatch } = this.props if (_isArticleEdited) { console.log('lockit') dispatch(lockStatus()) } else { console.log('unlockit') dispatch(unlockStatus()) } } }) } handleModeChange (value) { let { article } = this.state article.mode = value let _isModeChanged = article.mode !== this.props.activeArticle.mode let { isTagChanged, isContentChanged, isArticleEdited, isTitleChanged } = this.state let _isArticleEdited = _isModeChanged || isTagChanged || isContentChanged || isTitleChanged this.setState({ article, previewMode: false, isModeChanged: _isModeChanged, isArticleEdited: _isArticleEdited }, () => { if (isArticleEdited !== _isArticleEdited) { let { dispatch } = this.props if (_isArticleEdited) { console.log('lockit') dispatch(lockStatus()) } else { console.log('unlockit') dispatch(unlockStatus()) } } }) } handleModeSelectBlur () { if (this.refs.code != null) { this.refs.code.editor.focus() } } handleContentChange (e, value) { let { status } = this.props if (status.mode === IDLE_MODE) { return } let { article } = this.state article.content = value let _isContentChanged = article.content !== this.props.activeArticle.content let { isTagChanged, isModeChanged, isArticleEdited, isTitleChanged } = this.state let _isArticleEdited = _isContentChanged || isTagChanged || isModeChanged || isTitleChanged this.setState({ article, isContentChanged: _isContentChanged, isArticleEdited: _isArticleEdited }, () => { if (isArticleEdited !== _isArticleEdited) { let { dispatch } = this.props if (_isArticleEdited) { console.log('lockit') dispatch(lockStatus()) } else { console.log('unlockit') dispatch(unlockStatus()) } } }) } handleTogglePreviewButtonClick (e) { if (this.state.article.mode === 'markdown') { if (!this.state.previewMode) { let cursorPosition = this.refs.code.getCursorPosition() let firstVisibleRow = this.refs.code.getFirstVisibleRow() this.setState({ previewMode: true, cursorPosition, firstVisibleRow }, function () { let previewEl = ReactDOM.findDOMNode(this.refs.preview) let anchors = previewEl.querySelectorAll('.lineAnchor') for (let i = 0; i < anchors.length; i++) { if (parseInt(anchors[i].dataset.key, 10) > cursorPosition.row || i === anchors.length - 1) { var targetAnchor = anchors[i > 0 ? i - 1 : 0] previewEl.scrollTop = targetAnchor.offsetTop - 100 break } } }) } else { this.setState({ previewMode: false }, function () { console.log(this.state.cursorPosition) this.refs.code.moveCursorTo(this.state.cursorPosition.row, this.state.cursorPosition.column) this.refs.code.scrollToLine(this.state.firstVisibleRow) this.refs.code.editor.focus() }) } } } handleTitleKeyDown (e) { if (e.keyCode === 9 && !e.shiftKey) { e.preventDefault() this.refs.mode.handleIdleSelectClick() } } renderEdit () { let { folders, status, tags } = this.props let folderOptions = folders.map(folder => { return ( ) }) return (
{this.state.isArticleEdited ? ' (edited)' : ''} this.handleTagsChange(tags, tag)} suggestTags={tags} /> {status.isTutorialOpen ? tagSelectTutorialElement : null}
{ this.state.article.mode === 'markdown' ? () : null }
this.handleTitleKeyDown(e)} placeholder={this.state.article.createdAt == null ? `Created at ${moment().format('YYYY/MM/DD HH:mm')}` : 'Title'} ref='title' value={this.state.article.title} onChange={e => this.handleTitleChange(e)} />
this.handleModeChange(e)} value={this.state.article.mode} className='mode' onBlur={() => this.handleModeSelectBlur()} /> {status.isTutorialOpen ? modeSelectTutorialElement : null}
{this.state.previewMode ? : ( this.handleContentChange(e, value)} readOnly={false} mode={this.state.article.mode} code={this.state.article.content} />) }
) } render () { let { status, activeArticle } = this.props if (activeArticle == null) return this.renderEmpty() switch (status.mode) { case EDIT_MODE: return this.renderEdit() case IDLE_MODE: default: return this.renderIdle() } } } ArticleDetail.propTypes = { status: PropTypes.shape(), activeArticle: PropTypes.shape(), user: PropTypes.shape(), folders: PropTypes.array, dispatch: PropTypes.func } ArticleDetail.prototype.linkState = linkState