1
0
mirror of https://github.com/BoostIo/Boostnote synced 2026-02-13 16:00:40 +00:00

No edit mode

This commit is contained in:
Rokt33r
2015-12-28 00:59:36 +09:00
parent 013a1b4f51
commit 2537b6ba09
9 changed files with 240 additions and 364 deletions

View File

@@ -2,25 +2,16 @@ import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import moment from 'moment'
import _ from 'lodash'
import ModeIcon from 'browser/components/ModeIcon'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import CodeEditor from 'browser/components/CodeEditor'
import {
IDLE_MODE,
EDIT_MODE,
switchMode,
switchArticle,
switchFolder,
clearSearch,
lockStatus,
unlockStatus,
updateArticle,
destroyArticle,
NEW
cacheArticle,
saveArticle,
destroyArticle
} from '../../actions'
import linkState from 'browser/lib/linkState'
import FolderMark from 'browser/components/FolderMark'
import TagLink from '../TagLink'
import TagSelect from 'browser/components/TagSelect'
import ModeSelect from 'browser/components/ModeSelect'
import activityRecord from 'browser/lib/activityRecord'
@@ -123,45 +114,31 @@ export default class ArticleDetail extends React.Component {
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) {
if (nextProps.activeArticle == null || this.props.activeArticle == null || nextProps.activeArticle.key !== this.props.activeArticle.key) {
let nextArticle = nextProps.activeArticle
let nextModified = nextArticle != null ? _.findWhere(nextProps.modified, {key: nextArticle.key}) : null
let article = Object.assign({}, nextProps.activeArticle, nextModified)
this.setState({
article
})
}
}
componentWillReceiveProps (nextProps) {
let nextState = {}
cacheArticle () {
let { dispatch } = this.props
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)
dispatch(cacheArticle(this.props.activeArticle.key, this.state.article))
}
renderEmpty () {
return (
<div className='ArticleDetail empty'>
Command() + Enter to create a new post
<div className='ArticleDetail-empty-box'>
<div className='ArticleDetail-empty-box-message'>Command() + N<br/>to create a new post</div>
</div>
</div>
)
}
@@ -176,123 +153,46 @@ export default class ArticleDetail extends React.Component {
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})
let targetFolderKey = this.state.article.FolderKey
dispatch(saveArticle(this.props.activeArticle.key, this.state.article), true)
if (status.targetFolders.length > 0) {
let targetFolder = _.findWhere(folders, {key: targetFolderKey})
dispatch(switchFolder(targetFolder.name))
}
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})
this.setState({article: article}, () => this.cacheArticle())
}
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())
}
}
})
article
}, () => this.cacheArticle())
}
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())
}
}
})
article
}, () => this.cacheArticle())
}
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())
}
}
})
article
}, () => this.cacheArticle())
}
handleModeSelectBlur () {
@@ -302,34 +202,12 @@ export default class ArticleDetail extends React.Component {
}
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())
}
}
})
article
}, () => this.cacheArticle())
}
handleTogglePreviewButtonClick (e) {
@@ -373,14 +251,16 @@ export default class ArticleDetail extends React.Component {
}
render () {
let { folders, status, tags, activeArticle, user } = this.props
let { folders, status, tags, activeArticle, modified, user } = this.props
if (activeArticle == null) return this.renderEmpty()
let folderOptions = folders.map(folder => {
return (
<option key={folder.key} value={folder.key}>{folder.name}</option>
)
})
let isUnsaved = !!_.findWhere(modified, {key: activeArticle.key})
return (
<div className='ArticleDetail'>
<div className='ArticleDetail-info'>
@@ -392,7 +272,13 @@ export default class ArticleDetail extends React.Component {
>
{folderOptions}
</select>
{this.state.isArticleEdited ? ' (edited)' : ''}
<span className='ArticleDetail-info-status'
children={
isUnsaved
? <span> <span className='unsaved-mark'></span> Unsaved</span>
: `Created : ${moment(this.state.article.createdAt).format('YYYY/MM/DD')} Updated : ${moment(this.state.article.updatedAt).format('YYYY/MM/DD')}`
}
/>
<div className='ArticleDetail-info-control'>
<ShareButton
@@ -404,8 +290,8 @@ export default class ArticleDetail extends React.Component {
<i className='fa fa-fw fa-clipboard'/><span className='tooltip'>Copy to clipboard</span>
</button>
<button onClick={e => this.handleEditButtonClick(e)}>
<i className='fa fa-fw fa-edit'/><span className='tooltip'>Save ( + s)</span>
<button onClick={e => this.handleSaveButtonClick(e)}>
<i className='fa fa-fw fa-save'/><span className='tooltip'>Save ( + s)</span>
</button>
<button onClick={e => this.handleDeleteButtonClick(e)}>
<i className='fa fa-fw fa-trash'/><span className='tooltip'>Delete</span>
@@ -426,6 +312,15 @@ export default class ArticleDetail extends React.Component {
<div className='ArticleDetail-panel'>
<div className='ArticleDetail-panel-header'>
<div className='ArticleDetail-panel-header-title'>
<input
onKeyDown={e => this.handleTitleKeyDown(e)}
placeholder='(Untitled)'
ref='title'
value={this.state.article.title}
onChange={e => this.handleTitleChange(e)}
/>
</div>
<ModeSelect
ref='mode'
onChange={e => this.handleModeChange(e)}
@@ -433,17 +328,6 @@ export default class ArticleDetail extends React.Component {
className='ArticleDetail-panel-header-mode'
onBlur={() => this.handleModeSelectBlur()}
/>
<div className='ArticleDetail-panel-header-title'>
<input
onKeyDown={e => 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)}
/>
</div>
</div>
{status.isTutorialOpen ? modeSelectTutorialElement : null}
@@ -466,6 +350,7 @@ export default class ArticleDetail extends React.Component {
ArticleDetail.propTypes = {
status: PropTypes.shape(),
activeArticle: PropTypes.shape(),
modified: PropTypes.array,
user: PropTypes.shape(),
folders: PropTypes.array,
tags: PropTypes.array,

View File

@@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'browser/components/ModeIcon'
import moment from 'moment'
import { switchArticle, NEW } from '../actions'
import { switchArticle } from '../actions'
import FolderMark from 'browser/components/FolderMark'
import TagLink from './TagLink'
import _ from 'lodash'
@@ -64,40 +64,58 @@ export default class ArticleList extends React.Component {
handleArticleClick (article) {
let { dispatch } = this.props
return function (e) {
if (article.status === NEW) return null
dispatch(switchArticle(article.key))
}
}
render () {
let { articles, activeArticle, folders } = this.props
let { articles, modified, activeArticle, folders } = this.props
let articleElements = articles.map(article => {
let modifiedArticle = _.findWhere(modified, {key: article.key})
let originalArticle = article
if (modifiedArticle) {
article = Object.assign({}, article, modifiedArticle)
}
let tagElements = Array.isArray(article.tags) && article.tags.length > 0
? article.tags.map(tag => {
return (<TagLink key={tag} tag={tag}/>)
})
: (<span>Not tagged yet</span>)
let folder = _.findWhere(folders, {key: article.FolderKey})
let folderChanged = originalArticle.FolderKey !== article.FolderKey
let originalFolder = folderChanged ? _.findWhere(folders, {key: originalArticle.FolderKey}) : null
let title = article.status !== NEW
? article.title.trim().length === 0
let title = article.title.trim().length === 0
? <small>(Untitled)</small>
: article.title
: '(New article)'
return (
<div key={'article-' + article.key}>
<div onClick={e => this.handleArticleClick(article)(e)} className={'articleItem' + (activeArticle.key === article.key ? ' active' : '')}>
<div className='top'>
{folder != null
? <span className='folderName'><FolderMark color={folder.color}/>{folder.name}</span>
? folderChanged
? <span className='folderName'>
<FolderMark color={originalFolder.color}/>{originalFolder.name}
->
<FolderMark color={folder.color}/>{folder.name}
</span>
: <span className='folderName'>
<FolderMark color={folder.color}/>{folder.name}
</span>
: <span><FolderMark color={-1}/>Unknown</span>
}
<span className='updatedAt'>{article.status != null ? article.status : moment(article.updatedAt).fromNow()}</span>
<span className='updatedAt'
children={
modifiedArticle != null
? <span><span className='unsaved-mark'></span> Unsaved</span>
: moment(article.updatedAt).fromNow()
}
/>
</div>
<div className='middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>{title}</div>
<ModeIcon className='mode' mode={article.mode}/> <div className='title' children={title}/>
</div>
<div className='bottom'>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>
@@ -119,6 +137,7 @@ export default class ArticleList extends React.Component {
ArticleList.propTypes = {
folders: PropTypes.array,
articles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func
}

View File

@@ -1,6 +1,6 @@
import React, { PropTypes } from 'react'
import { findWhere } from 'lodash'
import { setSearchFilter, switchFolder, switchMode, switchArticle, updateArticle, clearNewArticle, EDIT_MODE } from '../actions'
import { setSearchFilter, switchFolder, saveArticle } from '../actions'
import { openModal } from 'browser/lib/modal'
import FolderMark from 'browser/components/FolderMark'
import Preferences from '../modal/Preferences'
@@ -71,20 +71,17 @@ export default class ArticleNavigator extends React.Component {
: folders[0].key
let newArticle = {
id: null,
key: keygen(),
title: '',
content: '',
mode: 'markdown',
tags: [],
FolderKey: FolderKey,
status: 'NEW'
craetedAt: new Date(),
updatedAt: new Date()
}
dispatch(clearNewArticle())
dispatch(updateArticle(newArticle))
dispatch(switchArticle(newArticle.key, true))
dispatch(switchMode(EDIT_MODE))
dispatch(saveArticle(newArticle.key, newArticle, true))
}
handleNewFolderButton (e) {

View File

@@ -0,0 +1,241 @@
import React, { PropTypes} from 'react'
import { connect } from 'react-redux'
import { toggleTutorial } from '../actions'
import ArticleNavigator from './ArticleNavigator'
import ArticleTopBar from './ArticleTopBar'
import ArticleList from './ArticleList'
import ArticleDetail from './ArticleDetail'
import _ from 'lodash'
import { isModalOpen, closeModal } from 'browser/lib/modal'
const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TAG_FILTER = 'TAG_FILTER'
class HomePage extends React.Component {
componentDidMount () {
// React自体のKey入力はfocusされていないElementからは動かないため、
// `window`に直接かける
this.keyHandler = e => this.handleKeyDown(e)
window.addEventListener('keydown', this.keyHandler)
}
componentWillUnmount () {
window.removeEventListener('keydown', this.keyHandler)
}
handleKeyDown (e) {
let cmdOrCtrl = process.platform === 'darwin' ? e.metaKey : e.ctrlKey
if (isModalOpen()) {
if (e.keyCode === 27) closeModal()
return
}
let { status, dispatch } = this.props
let { nav, top, list, detail } = this.refs
if (status.isTutorialOpen) {
dispatch(toggleTutorial())
e.preventDefault()
return
}
// Search inputがfocusされていたら大体のキー入力は無視される。
if (top.isInputFocused() && !(e.metaKey || e.ctrlKey)) {
if (e.keyCode === 13 || e.keyCode === 27) top.escape()
return
}
// `detail`の`openDeleteConfirmMenu`が`true`なら呼ばれない。
if (e.keyCode === 27 || (e.keyCode === 70 && cmdOrCtrl)) {
top.focusInput()
}
if (e.keyCode === 38) {
list.selectPriorArticle()
}
if (e.keyCode === 40) {
list.selectNextArticle()
}
if (e.keyCode === 78 && cmdOrCtrl) {
nav.handleNewPostButtonClick()
e.preventDefault()
}
}
render () {
let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags, filters } = this.props
return (
<div className='HomePage'>
<ArticleNavigator
ref='nav'
dispatch={dispatch}
user={user}
folders={folders}
status={status}
allArticles={allArticles}
/>
<ArticleTopBar
ref='top'
dispatch={dispatch}
status={status}
/>
<ArticleList
ref='list'
dispatch={dispatch}
folders={folders}
articles={articles}
modified={modified}
status={status}
activeArticle={activeArticle}
/>
<ArticleDetail
ref='detail'
dispatch={dispatch}
user={user}
activeArticle={activeArticle}
modified={modified}
folders={folders}
status={status}
tags={tags}
filters={filters}
/>
</div>
)
}
}
// Ignore invalid key
function ignoreInvalidKey (key) {
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/)
}
// Build filter object by key
function buildFilter (key) {
if (key.match(/^\/\/.+/)) {
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
}
if (key.match(/^\/.+/)) {
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
}
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
}
return {type: TEXT_FILTER, value: key}
}
function isContaining (target, needle) {
return target.match(new RegExp(_.escapeRegExp(needle)))
}
function startsWith (target, needle) {
return target.match(new RegExp('^' + _.escapeRegExp(needle)))
}
function remap (state) {
let { user, folders, status } = state
let _articles = state.articles
let articles = _articles != null ? _articles.data : []
let modified = _articles != null ? _articles.modified : []
articles.sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt)
})
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())
.filter(ignoreInvalidKey)
.map(buildFilter)
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
let targetFolders
if (folders != null) {
let exactTargetFolders = folders.filter(folder => {
return _.findWhere(folderExactFilters, {value: folder.name})
})
let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => startsWith(folder.name, filter.value))
})
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
if (targetFolders.length > 0) {
articles = articles.filter(article => {
return _.findWhere(targetFolders, {key: article.FolderKey})
})
}
if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => {
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
})
}, articles)
}
if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => {
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
})
}, articles)
}
}
// Grab active article
let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0]
return {
user,
folders,
status,
articles,
allArticles,
modified,
activeArticle,
tags,
filters: {
folder: folderFilters,
tag: tagFilters,
text: textFilters
}
}
}
HomePage.propTypes = {
status: PropTypes.shape(),
user: PropTypes.shape({
name: PropTypes.string
}),
articles: PropTypes.array,
allArticles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func,
folders: PropTypes.array,
filters: PropTypes.shape({
folder: PropTypes.array,
tag: PropTypes.array,
text: PropTypes.array
}),
tags: PropTypes.array
}
export default connect(remap)(HomePage)