1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 09:46:22 +00:00
This commit is contained in:
Dick Choi
2016-05-25 17:09:39 +09:00
parent 1a98afee92
commit 7f8733796e
17 changed files with 901 additions and 650 deletions

View File

@@ -2,31 +2,20 @@ import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import modes from '../lib/modes'
import _ from 'lodash'
import fetchConfig from '../lib/fetchConfig'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
const ace = window.ace
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.changeHandler = e => this.handleChange(e)
this.changeHandler = (e) => this.handleChange(e)
this.blurHandler = (e) => {
if (e.relatedTarget === null) {
return
}
let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some(clss => {
let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some((clss) => {
return clss === 'ace_search_field' || clss === 'ace_searchbtn' || clss === 'ace_replacebtn' || clss === 'ace_searchbtn_close' || clss === 'ace_text-input'
})
if (isFocusingToSearch) {
@@ -38,7 +27,7 @@ export default class CodeEditor extends React.Component {
this.killedBuffer = ''
this.execHandler = (e) => {
console.log(e.command.name)
console.info('ACE COMMAND >> %s', e.command.name)
switch (e.command.name) {
case 'gotolinestart':
e.preventDefault()
@@ -84,7 +73,7 @@ export default class CodeEditor extends React.Component {
this.afterExecHandler = (e) => {
switch (e.command.name) {
case 'find':
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), el => {
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), (el) => {
el.removeEventListener('blur', this.blurHandler)
el.addEventListener('blur', this.blurHandler)
})
@@ -93,11 +82,6 @@ export default class CodeEditor extends React.Component {
}
this.state = {
fontSize: config['editor-font-size'],
fontFamily: config['editor-font-family'],
indentType: config['editor-indent-type'],
indentSize: config['editor-indent-size'],
themeSyntax: config['theme-syntax']
}
this.silentChange = false
@@ -110,15 +94,15 @@ export default class CodeEditor extends React.Component {
}
componentDidMount () {
let { article } = this.props
var el = ReactDOM.findDOMNode(this)
var editor = this.editor = ace.edit(el)
let { mode, value } = this.props
let el = ReactDOM.findDOMNode(this)
let editor = this.editor = ace.edit(el)
editor.$blockScrolling = Infinity
editor.renderer.setShowGutter(true)
editor.setTheme('ace/theme/' + this.state.themeSyntax)
editor.setTheme('ace/theme/xcode')
editor.moveCursorTo(0, 0)
editor.setReadOnly(!!this.props.readOnly)
editor.setFontSize(this.state.fontSize)
editor.setFontSize('14')
editor.on('blur', this.blurHandler)
@@ -132,31 +116,19 @@ export default class CodeEditor extends React.Component {
readOnly: true
})
editor.commands.addCommand({
name: 'Emacs cursor up',
name: 'Emacs kill buffer',
bindKey: {mac: 'Ctrl-Y'},
exec: function (editor) {
editor.insert(this.killedBuffer)
}.bind(this),
readOnly: true
})
editor.commands.addCommand({
name: 'Focus title',
bindKey: {win: 'Esc', mac: 'Esc'},
exec: function (editor, e) {
let currentWindow = remote.getCurrentWebContents()
if (config['switch-preview'] === 'rightclick') {
currentWindow.send('detail-preview')
}
currentWindow.send('list-focus')
},
readOnly: true
})
editor.commands.on('exec', this.execHandler)
editor.commands.on('afterExec', this.afterExecHandler)
var session = editor.getSession()
let mode = _.findWhere(modes, {name: article.mode})
mode = _.find(modes, {name: mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
@@ -166,15 +138,12 @@ export default class CodeEditor extends React.Component {
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
session.setOption('useWorker', false)
session.setUseWrapMode(true)
session.setValue(this.props.article.content)
session.setValue(_.isString(value) ? value : '')
session.on('change', this.changeHandler)
ipc.on('config-apply', this.configApplyHandler)
}
componentWillUnmount () {
ipc.removeListener('config-apply', this.configApplyHandler)
this.editor.getSession().removeListener('change', this.changeHandler)
this.editor.removeListener('blur', this.blurHandler)
this.editor.commands.removeListener('exec', this.execHandler)
@@ -183,41 +152,37 @@ export default class CodeEditor extends React.Component {
componentDidUpdate (prevProps, prevState) {
var session = this.editor.getSession()
if (this.props.article.key !== prevProps.article.key) {
session.removeListener('change', this.changeHandler)
session.setValue(this.props.article.content)
session.getUndoManager().reset()
session.on('change', this.changeHandler)
}
if (prevProps.article.mode !== this.props.article.mode) {
let mode = _.findWhere(modes, {name: this.props.article.mode})
if (prevProps.mode !== this.props.mode) {
let mode = _.find(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
session.setMode('ace/mode' + syntaxMode)
}
}
handleConfigApply (e, config) {
this.setState({
fontSize: config['editor-font-size'],
fontFamily: config['editor-font-family'],
indentType: config['editor-indent-type'],
indentSize: config['editor-indent-size'],
themeSyntax: config['theme-syntax']
}, function () {
var editor = this.editor
editor.setTheme('ace/theme/' + this.state.themeSyntax)
// this.setState({
// fontSize: config['editor-font-size'],
// fontFamily: config['editor-font-family'],
// indentType: config['editor-indent-type'],
// indentSize: config['editor-indent-size'],
// themeSyntax: config['theme-syntax']
// }, function () {
// var editor = this.editor
// editor.setTheme('ace/theme/' + this.state.themeSyntax)
var session = editor.getSession()
session.setUseSoftTabs(this.state.indentType === 'space')
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
})
// var session = editor.getSession()
// session.setUseSoftTabs(this.state.indentType === 'space')
// session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
// })
}
handleChange (e) {
if (this.props.onChange) {
var value = this.editor.getValue()
this.props.onChange(value)
this.value = this.editor.getValue()
this.props.onChange(e)
}
}
@@ -237,13 +202,34 @@ export default class CodeEditor extends React.Component {
this.editor.scrollToLine(num, false, false)
}
focus () {
this.editor.focus()
}
blur () {
this.editor.blur()
}
reload () {
let session = this.editor.getSession()
session.removeListener('change', this.changeHandler)
session.setValue(this.props.value)
session.getUndoManager().reset()
session.on('change', this.changeHandler)
}
render () {
let { className } = this.props
return (
<div
className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}
className={className == null
? 'CodeEditor'
: `CodeEditor ${className}`
}
style={{
fontSize: this.state.fontSize,
fontFamily: this.state.fontFamily.trim() + ', monospace'
fontSize: '14px',
fontFamily: 'monospace'
}}
/>
)
@@ -251,11 +237,8 @@ export default class CodeEditor extends React.Component {
}
CodeEditor.propTypes = {
article: PropTypes.shape({
content: PropTypes.string,
mode: PropTypes.string,
key: PropTypes.string
}),
value: PropTypes.string,
mode: PropTypes.string,
className: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,

View File

@@ -0,0 +1,81 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './MarkdownEditor.styl'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
class MarkdownEditor extends React.Component {
constructor (props) {
super(props)
this.state = {
status: 'CODE'
}
}
handleChange (e) {
this.value = this.refs.code.value
this.props.onChange(e)
}
handleContextMenu (e) {
let newStatus = this.state.status === 'PREVIEW'
? 'CODE'
: 'PREVIEW'
this.setState({
status: newStatus
}, () => {
if (newStatus === 'CODE') {
this.refs.code.focus()
} else {
this.refs.code.blur()
this.refs.preview.focus()
}
})
}
reload () {
this.refs.code.reload()
}
render () {
let { className, value } = this.props
return (
<div className={className == null
? 'MarkdownEditor'
: `MarkdownEditor ${className}`
}
onContextMenu={(e) => this.handleContextMenu(e)}
>
<CodeEditor styleName='codeEditor'
ref='code'
mode='markdown'
value={value}
onChange={(e) => this.handleChange(e)}
/>
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview'
: 'preview--hide'
}
style={this.props.ignorePreviewPointerEvents
? {pointerEvents: 'none'} : {}
}
ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)}
tabIndex='0'
value={value}
/>
</div>
)
}
}
MarkdownEditor.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(MarkdownEditor, styles)

View File

@@ -0,0 +1,23 @@
.root
position relative
.codeEditor
absolute top bottom left right
.codeEditor--hide
@extend .codeEditor
.preview
display block
absolute top bottom left right
z-index 100
background-color white
height 100%
width 100%
.preview--hide
@extend .preview
z-index 0
opacity 0
pointer-events none

View File

@@ -1,214 +1,81 @@
import React, { PropTypes } from 'react'
import markdown from '../lib/markdown'
import ReactDOM from 'react-dom'
import sanitizeHtml from '@rokt33r/sanitize-html'
import _ from 'lodash'
import fetchConfig from '../lib/fetchConfig'
import markdown from 'browser/lib/markdown'
const electron = require('electron')
const shell = electron.shell
const ipc = electron.ipcRenderer
const katex = window.katex
const OSX = global.process.platform === 'darwin'
const sanitizeOpts = {
allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'span', 'cite', 'del', 'u', 'sub', 'sup', 's', 'input', 'label' ],
allowedClasses: {
'a': ['lineAnchor'],
'div': ['math'],
'pre': ['hljs'],
'span': ['math', 'hljs-*', 'lineNumber'],
'code': ['language-*']
},
allowedAttributes: {
a: ['href', 'data-key'],
img: [ 'src' ],
label: ['for'],
input: ['checked', 'type'],
'*': ['id', 'name']
},
transformTags: {
'*': function (tagName, attribs) {
let href = attribs.href
if (tagName === 'input' && attribs.type !== 'checkbox') {
return false
}
if (_.isString(href) && href.match(/^#.+$/)) attribs.href = href.replace(/^#/, '#md-anchor-')
if (attribs.id) attribs.id = 'md-anchor-' + attribs.id
if (attribs.name) attribs.name = 'md-anchor-' + attribs.name
if (attribs.for) attribs.for = 'md-anchor-' + attribs.for
return {
tagName: tagName,
attribs: attribs
}
}
}
}
function handleAnchorClick (e) {
if (this.attributes.href && this.attributes.href.nodeValue.match(/^#.+/)) {
return
}
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const { shell } = require('electron')
const goExternal = function (e) {
e.preventDefault()
e.stopPropagation()
let href = this.href
if (href && href.match(/^http:\/\/|https:\/\/|mailto:\/\//)) {
shell.openExternal(href)
}
shell.openExternal(e.target.href)
}
function stopPropagation (e) {
e.preventDefault()
e.stopPropagation()
}
function math2Katex (display) {
return function (el) {
try {
katex.render(el.innerHTML.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&'), el, {display: display})
el.className = 'math-rendered'
} catch (e) {
el.innerHTML = e.message
el.className = 'math-failed'
}
}
}
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
export default class MarkdownPreview extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.state = {
fontSize: config['preview-font-size'],
fontFamily: config['preview-font-family'],
lineNumber: config['preview-line-number']
}
this.contextMenuHandler = (e) => this.handleContextMenu(e)
}
handleContextMenu (e) {
this.props.onContextMenu(e)
}
componentDidMount () {
this.addListener()
this.renderMath()
ipc.on('config-apply', this.configApplyHandler)
}
componentDidUpdate () {
this.addListener()
this.renderMath()
this.refs.root.setAttribute('sandbox', 'allow-same-origin')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
this.rewriteIframe()
}
componentWillUnmount () {
this.removeListener()
ipc.removeListener('config-apply', this.configApplyHandler)
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
}
componentWillUpdate () {
this.removeListener()
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe()
}
renderMath () {
let inline = ReactDOM.findDOMNode(this).querySelectorAll('span.math')
Array.prototype.forEach.call(inline, math2Katex(false))
let block = ReactDOM.findDOMNode(this).querySelectorAll('div.math')
Array.prototype.forEach.call(block, math2Katex(true))
}
addListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
Array.prototype.forEach.call(anchors, anchor => {
anchor.addEventListener('click', handleAnchorClick)
anchor.addEventListener('mousedown', stopPropagation)
anchor.addEventListener('mouseup', stopPropagation)
rewriteIframe () {
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.removeEventListener('click', goExternal)
})
Array.prototype.forEach.call(inputs, input => {
input.addEventListener('click', stopPropagation)
let { value } = this.props
this.refs.root.contentWindow.document.head.innerHTML = `
<style>
@font-face {
font-family: 'Lato';
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
${markdownStyle}
</style>
<link rel="stylesheet" href="../node_modules/highlight.js/styles/xcode.css" id="hljs-css">
<link rel="stylesheet" href="../resources/katex.min.css">
`
this.refs.root.contentWindow.document.body.innerHTML = markdown(value)
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.addEventListener('click', goExternal)
})
}
removeListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
Array.prototype.forEach.call(anchors, anchor => {
anchor.removeEventListener('click', handleAnchorClick)
anchor.removeEventListener('mousedown', stopPropagation)
anchor.removeEventListener('mouseup', stopPropagation)
})
Array.prototype.forEach.call(inputs, input => {
input.removeEventListener('click', stopPropagation)
})
}
handleClick (e) {
if (this.props.onClick) {
this.props.onClick(e)
}
}
handleDoubleClick (e) {
if (this.props.onDoubleClick) {
this.props.onDoubleClick(e)
}
}
handleMouseDown (e) {
if (this.props.onMouseDown) {
this.props.onMouseDown(e)
}
}
handleMouseUp (e) {
if (this.props.onMouseUp) {
this.props.onMouseUp(e)
}
}
handleMouseMove (e) {
if (this.props.onMouseMove) {
this.props.onMouseMove(e)
}
}
handleConfigApply (e, config) {
this.setState({
fontSize: config['preview-font-size'],
fontFamily: config['preview-font-family'],
lineNumber: config['preview-line-number']
})
focus () {
this.refs.root.focus()
}
render () {
let isEmpty = this.props.content.trim().length === 0
let content = isEmpty
? '(Empty content)'
: this.props.content
content = markdown(content)
content = sanitizeHtml(content, sanitizeOpts)
let { className, style, tabIndex } = this.props
return (
<div
className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '') + (isEmpty ? ' empty' : '') + (this.state.lineNumber ? ' lineNumbered' : '')}
onClick={e => this.handleClick(e)}
onDoubleClick={e => this.handleDoubleClick(e)}
onMouseDown={e => this.handleMouseDown(e)}
onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}
dangerouslySetInnerHTML={{__html: ' ' + content}}
style={{
fontSize: this.state.fontSize,
fontFamily: this.state.fontFamily.trim() + (OSX ? '' : ', meiryo, \'Microsoft YaHei\'') + ', helvetica, arial, sans-serif'
}}
<iframe className={className != null
? 'MarkdownPreview ' + className
: 'MarkdownPreview'
}
style={style}
tabIndex={tabIndex}
ref='root'
/>
)
}
@@ -221,5 +88,5 @@ MarkdownPreview.propTypes = {
onMouseDown: PropTypes.func,
onMouseMove: PropTypes.func,
className: PropTypes.string,
content: PropTypes.string
value: PropTypes.string
}

View File

@@ -0,0 +1,252 @@
global-reset()
borderColor = #D0D0D0 // using
highlightenBorderColor = darken(borderColor, 20%)
invBorderColor = #404849
brandBorderColor = #3FB399
focusBorderColor = #369DCD
buttonBorderColor = #4C4C4C
lightButtonColor = #898989
hoverBackgroundColor= transparentify(#444, 4%) // using
inactiveTextColor = #888 // using
textColor = #4D4D4D // using
backgroundColor= white
fontSize= 14px // using
shadowColor= #C5C5C5
invBackgroundColor = #4C4C4C
invTextColor = white
btnColor = #888
btnHighlightenColor = #000
brandColor = #2BAC8F
popupShadow = 0 0 5px 0 #888
tableHeadBgColor = white
tableOddBgColor = #F9F9F9
tableEvenBgColor = white
facebookColor= #3b5998
githubBtn= #201F1F
// using
successBackgroundColor= #E0F0D9
successTextColor= #3E753F
errorBackgroundColor= #F2DEDE
errorTextColor= #A64444
infoBackgroundColor= #D9EDF7
infoTextColor= #34708E
popupZIndex= 500
body
font-size 16px
padding 15px
font-family helvetica, arial, sans-serif
line-height 1.6
overflow-x hidden
.katex
font 400 1.2em 'KaTeX_Main'
line-height 1.2em
white-space nowrap
text-indent 0
.katex .mfrac>.vlist>span:nth-child(2)
top 0 !important
.katex-error
background-color errorBackgroundColor
color errorTextColor
padding 5px
margin -5px
border-radius 5px
div.math-rendered
text-align center
.math-failed
background-color alpha(red, 0.1)
color darken(red, 15%)
padding 5px
margin 5px 0
border-radius 5px
sup
position relative
top -.4em
font-size 0.8em
vertical-align top
sub
position relative
bottom -.4em
font-size 0.8em
vertical-align top
a
color brandColor
text-decoration none
padding 5px
border-radius 5px
margin -5px
transition .1s
display inline-block
img
vertical-align sub
&:hover
color lighten(brandColor, 5%)
text-decoration underline
background-color alpha(#FFC95C, 0.3)
&:visited
color brandColor
&.lineAnchor
padding 0
margin 0
display block
font-size 0
height 0
hr
border-top none
border-bottom solid 1px borderColor
margin 15px 0
h1, h2, h3, h4, h5, h6
font-weight bold
h1
font-size 2.25em
padding-bottom 0.3em
line-height 1.2em
border-bottom solid 1px borderColor
margin 1em 0 0.44em
&:first-child
margin-top 0
h2
font-size 1.75em
padding-bottom 0.3em
line-height 1.225em
border-bottom solid 1px borderColor
margin 1em 0 0.57em
&:first-child
margin-top 0
h3
font-size 1.5em
line-height 1.43em
margin 1em 0 0.66em
h4
font-size 1.25em
line-height 1.4em
margin 1em 0 0.8em
h5
font-size 1em
line-height 1.4em
margin 1em 0 1em
h6
font-size 1em
line-height 1.4em
margin 1em 0 1em
color #777
*:not(a.lineAnchor) + p, *:not(a.lineAnchor) + blockquote, *:not(a.lineAnchor) + ul, *:not(a.lineAnchor) + ol, *:not(a.lineAnchor) + pre
margin-top 1em
p
line-height 1.6em
margin 0 0 1em
white-space pre-line
img
max-width 100%
strong, b
font-weight bold
em, i
font-style italic
s, del, strike
text-decoration line-through
u
text-decoration underline
blockquote
border-left solid 4px brandBorderColor
margin 0 0 1em
padding 0 25px
ul
list-style-type disc
padding-left 2em
margin-bottom 1em
li
display list-item
&>li>ul, &>li>ol
margin 0
&>li>ul
list-style-type circle
&>li>ul
list-style-type square
ol
list-style-type decimal
padding-left 2em
margin-bottom 1em
li
display list-item
&>li>ul, &>li>ol
margin 0
code
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
padding 0.2em 0.4em
background-color #f7f7f7
border-radius 3px
font-size 0.85em
text-decoration none
margin-right 2px
*:not(a.lineAnchor) + code
margin-left 2px
pre
padding 1em !important
background-color #f7f7f7 !important
border-radius 5px
overflow-x auto
margin 0 0 1em
line-height 1.35
code
margin 0
padding 0
border none
border-radius 0
pre
border none
margin -5px
&>span.lineNumber
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
display none
float left
margin 0 0.5em 0 -0.5em
border-right 1px solid
text-align right
&>span
display block
padding 0 .5em 0 1em
table
display block
width 100%
margin 0 0 1em
thead
tr
background-color tableHeadBgColor
th
border-style solid
padding 6px 13px
line-height 1.6
border-width 1px 0 2px 1px
border-color borderColor
&:last-child
border-right solid 1px borderColor
tbody
tr:nth-child(2n + 1)
background-color tableOddBgColor
tr:nth-child(2n)
background-color tableEvenBgColor
td
border-style solid
padding 6px 13px
line-height 1.6
border-width 0 0 1px 1px
border-color borderColor
&:last-child
border-right solid 1px borderColor

View File

@@ -149,16 +149,16 @@ class Repository {
let fetchNotes = () => {
let noteNames = fs.readdirSync(dataPath)
let notes = noteNames
.map((noteName) => {
let notePath = path.join(dataPath, noteName)
.map((noteName) => path.join(dataPath, noteName))
.filter((notePath) => CSON.isObjectPath(notePath))
.map((notePath) => {
return new Promise(function (resolve, reject) {
CSON.readFile(notePath, function (err, obj) {
if (err != null) {
console.log(err)
return resolve(null)
}
obj.key = path.basename(noteName, '.cson')
obj.key = path.basename(notePath, '.cson')
return resolve(obj)
})
})
@@ -427,24 +427,16 @@ class Repository {
}
updateNote (noteKey, override) {
let note = _.find(this.notes, {key: noteKey})
let isNew = false
if (note == null) {
note = override
isNew = true
}
if (!this.constructor.validateNote(note)) {
if (!this.constructor.validateNote(override)) {
return Promise.reject(new Error('Invalid input'))
}
if (isNew) this.notes.push(note)
note.updatedAt = new Date()
override.updatedAt = new Date()
return new Promise((resolve, reject) => {
CSON.writeFile(path.join(this.cached.path, 'data', note.key + '.cson'), _.omit(note, ['key']), function (err) {
CSON.writeFile(path.join(this.cached.path, 'data', noteKey + '.cson'), _.omit(override, ['key']), function (err) {
if (err != null) return reject(err)
resolve(note)
override.key = noteKey
resolve(override)
})
})
}

View File

@@ -3,6 +3,8 @@ import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math'
import hljs from 'highlight.js'
const katex = window.katex
function createGutter (str) {
let lc = (str.match(/\n/g) || []).length
let lines = []
@@ -39,10 +41,22 @@ md.use(emoji, {
})
md.use(math, {
inlineRenderer: function (str) {
return `<span class='math'>${str}</span>`
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
return `<div class='math'>${str}</div>`
let output = ''
try {
output = katex.renderToString(str.trim(), {displayMode: true})
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
md.use(require('markdown-it-checkbox'))

View File

@@ -8,10 +8,10 @@
height 320px
display flex
align-items center
.empty-message
width 100%
font-size 42px
line-height 72px
text-align center
color $ui-inactive-text-color

View File

@@ -0,0 +1,155 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteDetail.styl'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import queue from 'browser/main/lib/queue'
class NoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
note: Object.assign({}, props.note),
isDispatchQueued: false
}
this.dispatchTimer = null
}
componentDidUpdate (prevProps, prevState) {
}
componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key) {
if (this.state.isDispatchQueued) {
this.dispatch()
}
this.setState({
note: Object.assign({}, nextProps.note),
isDispatchQueued: false
}, () => {
this.refs.content.reload()
})
}
}
findTitle (value) {
let splitted = value.split('\n')
let title = null
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.match(/^# .+/)) {
title = trimmedLine.substring(1, trimmedLine.length).trim()
break
}
}
if (title == null) {
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.length > 0) {
title = trimmedLine
break
}
}
if (title == null) {
title = ''
}
}
return title
}
handleChange (e) {
let { note } = this.state
note.content = this.refs.content.value
this.setState({
note,
isDispatchQueued: true
}, () => {
this.queueDispatch()
})
}
cancelDispatchQueue () {
if (this.dispatchTimer != null) {
window.clearTimeout(this.dispatchTimer)
this.dispatchTimer = null
}
}
queueDispatch () {
this.cancelDispatchQueue()
this.dispatchTimer = window.setTimeout(() => {
this.dispatch()
this.setState({
isDispatchQueued: false
})
}, 500)
}
dispatch () {
let { note } = this.state
note = Object.assign({}, note)
let repoKey = note._repository.key
note.title = this.findTitle(note.content)
let { dispatch } = this.props
dispatch({
type: 'SAVE_NOTE',
repository: repoKey,
note: note
})
queue.save(repoKey, note)
}
render () {
return (
<div className='NoteDetail'
style={this.props.style}
styleName='root'
>
<div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-folderSelect'>FOLDER SELECT</div>
<div styleName='info-left-tagSelect'>TAG SELECT</div>
</div>
<div styleName='info-right'>
<button styleName='info-right-button'>
<i className='fa fa-clipboard fa-fw'/>
</button>
<button styleName='info-right-button'>
<i className='fa fa-share-alt fa-fw'/>
</button>
<button styleName='info-right-button'>
<i className='fa fa-ellipsis-v'/>
</button>
</div>
</div>
<div styleName='body'>
<MarkdownEditor
ref='content'
styleName='body-noteEditor'
value={this.state.note.content}
onChange={(e) => this.handleChange(e)}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
/>
</div>
</div>
)
}
}
NoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(NoteDetail, styles)

View File

@@ -0,0 +1,38 @@
.root
absolute top bottom right
border-width 1px 0
border-style solid
border-color $ui-borderColor
.info
absolute top left right
height 50px
border-bottom $ui-border
background-color $ui-backgroundColor
.info-left
float left
.info-right
float right
.info-right-button
width 34px
height 34px
border-radius 17px
navButtonColor()
border $ui-border
font-size 14px
margin 8px 2px
padding 0
&:active
border-color $ui-button--active-backgroundColor
&:hover .left-control-newPostButton-tooltip
display block
.body
absolute bottom left right
top 50px
.body-noteEditor
absolute top bottom left right

View File

@@ -0,0 +1,65 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Detail.styl'
import _ from 'lodash'
import NoteDetail from './NoteDetail'
const electron = require('electron')
const OSX = global.process.platform === 'darwin'
class Detail extends React.Component {
componentDidUpdate (prevProps, prevState) {
}
render () {
let { repositories, location } = this.props
let note = null
if (location.query.key != null) {
let splitted = location.query.key.split('-')
let repoKey = splitted.shift()
let noteKey = splitted.shift()
let repo = _.find(repositories, {key: repoKey})
if (_.isObject(repo) && _.isArray(repo.notes)) {
note = _.find(repo.notes, {key: noteKey})
}
}
if (note == null) {
return (
<div className='Detail'
style={this.props.style}
styleName='root'
tabIndex='0'
>
<div styleName='empty'>
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
</div>
</div>
)
}
return (
<NoteDetail
note={note}
{..._.pick(this.props, [
'dispatch',
'repositories',
'style',
'ignorePreviewPointerEvents'
])}
/>
)
}
}
Detail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(Detail, styles)

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux'
import SideNav from './SideNav'
import TopBar from './TopBar'
import NoteList from './NoteList'
import NoteDetail from './NoteDetail'
import Detail from './Detail'
import Repository from 'browser/lib/Repository'
import StatusBar from './StatusBar'
import _ from 'lodash'
@@ -111,8 +111,16 @@ class Main extends React.Component {
>
<div styleName='slider-hitbox'/>
</div>
<NoteDetail
<Detail
style={{left: this.state.listWidth + 1}}
{..._.pick(this.props, [
'dispatch',
'repositories',
'config',
'params',
'location'
])}
ignorePreviewPointerEvents={this.state.isSliderFocused}
/>
</div>
<StatusBar

View File

@@ -1,243 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import CodeEditor from 'browser/components/CodeEditor'
import activityRecord from 'browser/lib/activityRecord'
import fetchConfig from 'browser/lib/fetchConfig'
const electron = require('electron')
const ipc = electron.ipcRenderer
export const PREVIEW_MODE = 'PREVIEW_MODE'
export const EDIT_MODE = 'EDIT_MODE'
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
export default class ArticleEditor extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.isMouseDown = false
this.state = {
status: PREVIEW_MODE,
cursorPosition: null,
firstVisibleRow: null,
switchPreview: config['switch-preview'],
isTemporary: false
}
}
componentDidMount () {
ipc.on('config-apply', this.configApplyHandler)
}
componentWillUnmount () {
ipc.removeListener('config-apply', this.configApplyHandler)
}
componentWillReceiveProps (nextProps) {
if (nextProps.article.key !== this.props.article.key) {
this.setState({
content: this.props.article.content
})
}
}
handleConfigApply (e, newConfig) {
this.setState({
switchPreview: newConfig['switch-preview']
})
}
resetCursorPosition () {
this.setState({
cursorPosition: null,
firstVisibleRow: null
}, function () {
let previewEl = ReactDOM.findDOMNode(this.refs.preview)
if (previewEl) previewEl.scrollTop = 0
})
}
switchPreviewMode (isTemporary = false) {
if (this.props.article.mode !== 'markdown') return true
let cursorPosition = this.refs.editor.getCursorPosition()
let firstVisibleRow = this.refs.editor.getFirstVisibleRow()
this.setState({
status: PREVIEW_MODE,
cursorPosition,
firstVisibleRow,
isTemporary: isTemporary
}, 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
}
}
})
}
switchEditMode (isTemporary = false) {
this.setState({
status: EDIT_MODE,
isTemporary: false
}, function () {
if (this.state.cursorPosition != null) {
this.refs.editor.moveCursorTo(this.state.cursorPosition.row, this.state.cursorPosition.column)
this.refs.editor.scrollToLine(this.state.firstVisibleRow)
}
this.refs.editor.editor.focus()
if (!isTemporary) activityRecord.emit('ARTICLE_UPDATE', this.props.article)
})
}
handleBlurCodeEditor (e) {
let isFocusingToThis = e.relatedTarget === ReactDOM.findDOMNode(this)
if (isFocusingToThis || this.state.switchPreview !== 'blur') {
return
}
let { article } = this.props
if (article.mode === 'markdown') {
this.switchPreviewMode()
}
}
handleCodeEditorChange (value) {
this.props.onChange(value)
}
handleRightClick (e) {
let { article } = this.props
if (this.state.switchPreview === 'rightclick' && article.mode === 'markdown') {
if (this.state.status === EDIT_MODE) this.switchPreviewMode()
else this.switchEditMode()
}
}
handleMouseUp (e) {
let { article } = this.props
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
if (!showPreview) {
return false
}
switch (this.state.switchPreview) {
case 'blur':
switch (e.button) {
case 0:
this.isMouseDown = false
this.moveCount = 0
if (!this.isDrag) {
this.switchEditMode()
}
break
case 2:
if (this.state.isTemporary) this.switchEditMode(true)
}
break
case 'rightclick':
}
}
handleMouseMove (e) {
let { article } = this.props
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
if (!showPreview) {
return false
}
if (this.state.switchPreview === 'blur' && this.isMouseDown) {
this.moveCount++
if (this.moveCount > 5) {
this.isDrag = true
}
}
}
handleMouseDowm (e) {
let { article } = this.props
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
if (!showPreview) {
return false
}
switch (this.state.switchPreview) {
case 'blur':
switch (e.button) {
case 0:
this.isDrag = false
this.isMouseDown = true
this.moveCount = 0
break
case 2:
if (this.state.status === EDIT_MODE && this.props.article.mode === 'markdown') {
this.switchPreviewMode(true)
}
}
break
case 'rightclick':
}
}
render () {
let { article } = this.props
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
return (
<div
tabIndex='5'
onContextMenu={e => this.handleRightClick(e)}
onMouseUp={e => this.handleMouseUp(e)}
onMouseMove={e => this.handleMouseMove(e)}
onMouseDown={e => this.handleMouseDowm(e)}
className='ArticleEditor'
>
{showPreview
? <MarkdownPreview
ref='preview'
content={article.content}
/>
: <CodeEditor
ref='editor'
onBlur={e => this.handleBlurCodeEditor(e)}
onChange={value => this.handleCodeEditorChange(value)}
article={article}
/>
}
{article.mode === 'markdown'
? <div className='ArticleDetail-panel-content-tooltip' children={
showPreview
? this.state.switchPreview === 'blur'
? 'Click to Edit'
: 'Right Click to Edit'
: this.state.switchPreview === 'blur'
? 'Press ESC to Watch Preview'
: 'Right Click to Watch Preview'
}
/>
: null
}
</div>
)
}
}
ArticleEditor.propTypes = {
article: PropTypes.shape({
content: PropTypes.string,
key: PropTypes.string,
mode: PropTypes.string
}),
onChange: PropTypes.func,
parent: PropTypes.object
}

View File

@@ -1,45 +0,0 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteDetail.styl'
const electron = require('electron')
const OSX = global.process.platform === 'darwin'
class NoteDetail extends React.Component {
componentDidUpdate (prevProps, prevState) {
}
renderEmpty () {
return (
<div styleName='empty'>
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
</div>
)
}
render () {
let isEmpty = true
let view = isEmpty
? this.renderEmpty()
: null
return (
<div className='NoteDetail'
style={this.props.style}
styleName='root'
tabIndex='0'
>
{view}
</div>
)
}
}
NoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
style: PropTypes.shape({
left: PropTypes.number
})
}
export default CSSModules(NoteDetail, styles)

View File

@@ -1,15 +1,9 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteList.styl'
import ReactDOM from 'react-dom'
import ModeIcon from 'browser/components/ModeIcon'
import moment from 'moment'
import _ from 'lodash'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
class NoteList extends React.Component {
constructor (props) {
super(props)
@@ -64,63 +58,56 @@ class NoteList extends React.Component {
// 移動ができなかったらfalseを返す:
selectPriorArticle () {
let { articles, activeArticle, dispatch } = this.props
let targetIndex = articles.indexOf(activeArticle) - 1
let targetArticle = articles[targetIndex]
return false
// let { articles, activeArticle, dispatch } = this.props
// let targetIndex = articles.indexOf(activeArticle) - 1
// let targetArticle = articles[targetIndex]
// return false
}
selectNextArticle () {
let { articles, activeArticle, dispatch } = this.props
let targetIndex = articles.indexOf(activeArticle) + 1
let targetArticle = articles[targetIndex]
// let { articles, activeArticle, dispatch } = this.props
// let targetIndex = articles.indexOf(activeArticle) + 1
// let targetArticle = articles[targetIndex]
if (targetArticle != null) {
dispatch(switchArticle(targetArticle.key))
return true
}
return false
}
handleArticleClick (article) {
let { dispatch } = this.props
return function (e) {
dispatch(switchArticle(article.key))
}
// if (targetArticle != null) {
// dispatch(switchArticle(targetArticle.key))
// return true
// }
// return false
}
handleNoteListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true
if (e.keyCode === 65 && !e.shiftKey) {
e.preventDefault()
remote.getCurrentWebContents().send('top-new-post')
}
// if (e.keyCode === 65 && !e.shiftKey) {
// e.preventDefault()
// remote.getCurrentWebContents().send('top-new-post')
// }
if (e.keyCode === 65 && e.shiftKey) {
e.preventDefault()
remote.getCurrentWebContents().send('nav-new-folder')
}
// if (e.keyCode === 65 && e.shiftKey) {
// e.preventDefault()
// remote.getCurrentWebContents().send('nav-new-folder')
// }
if (e.keyCode === 68) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-delete')
}
// if (e.keyCode === 68) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-delete')
// }
if (e.keyCode === 84) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-title')
}
// if (e.keyCode === 84) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-title')
// }
if (e.keyCode === 69) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-edit')
}
// if (e.keyCode === 69) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-edit')
// }
if (e.keyCode === 83) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-save')
}
// if (e.keyCode === 83) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-save')
// }
if (e.keyCode === 38) {
e.preventDefault()
@@ -186,41 +173,42 @@ class NoteList extends React.Component {
render () {
let { location } = this.props
let notes = this.notes = this.getNotes()
let noteElements = notes.map((note) => {
let folder = _.find(note._repository.folders, {key: note.folder})
let tagElements = note.tags.map((tag) => {
return <span key='tag'>{tag}</span>
})
let key = `${note._repository.key}-${note.key}`
let isActive = location.query.key === key
return (
<div styleName={isActive
? 'item--active'
: 'item'
}
key={key}
onClick={(e) => this.handleNoteClick(key)(e)}
>
<div styleName='item-border'/>
<div styleName='item-info'>
let noteElements = notes
.map((note) => {
let folder = _.find(note._repository.folders, {key: note.folder})
let tagElements = note.tags.map((tag) => {
return <span key='tag'>{tag}</span>
})
let key = `${note._repository.key}-${note.key}`
let isActive = location.query.key === key
return (
<div styleName={isActive
? 'item--active'
: 'item'
}
key={key}
onClick={(e) => this.handleNoteClick(key)(e)}
>
<div styleName='item-border'/>
<div styleName='item-info'>
<div styleName='item-info-left'>
<i className='fa fa-cube fa-fw' style={{color: folder.color}}/> {folder.name}
</div>
<div styleName='item-info-right'>
{moment(note.createdAt).fromNow()}
</div>
<div styleName='item-info-left'>
<i className='fa fa-cube fa-fw' style={{color: folder.color}}/> {folder.name}
</div>
<div styleName='item-info-right'>
{moment(note.createdAt).fromNow()}
</div>
<div styleName='item-title'>{note.title}</div>
<div styleName='item-tags'><i className='fa fa-tags fa-fw'/>{tagElements.length > 0 ? tagElements : <span>Not tagged yet</span>}</div>
</div>
<div styleName='item-title'>{note.title}</div>
<div styleName='item-tags'><i className='fa fa-tags fa-fw'/>{tagElements.length > 0 ? tagElements : <span>Not tagged yet</span>}</div>
</div>
)
})
)
})
return (
<div className='NoteList'

57
browser/main/lib/queue.js Normal file
View File

@@ -0,0 +1,57 @@
import Repository from 'browser/lib/Repository'
import _ from 'lodash'
let tasks = []
function _save (task, repoKey, note) {
delete note._repository
task.status = 'process'
Repository
.find(repoKey)
.then((repo) => {
return repo.updateNote(note.key, note)
})
.then((note) => {
tasks.splice(tasks.indexOf(task), 1)
console.log(tasks)
console.info('Note saved', note)
})
.catch((err) => {
tasks.splice(tasks.indexOf(task), 1)
console.error('Failed to save note', note)
console.error(err)
})
}
const queueSaving = function (repoKey, note) {
let key = `${repoKey}-${note.key}`
let taskIndex = _.findIndex(tasks, {
type: 'SAVE_NOTE',
key: key,
status: 'idle'
})
let task = tasks[taskIndex]
if (taskIndex < 0) {
task = {
type: 'SAVE_NOTE',
key: key,
status: 'idle',
timer: null
}
} else {
tasks.splice(taskIndex, 1)
window.clearTimeout(task.timer)
}
task.timer = window.setTimeout(() => {
_save(task, repoKey, note)
}, 1500)
tasks.push(task)
}
export default {
save: queueSaving
}

View File

@@ -115,6 +115,22 @@ function repositories (state = initialRepositories, action) {
if (targetRepo == null) return state
targetRepo.notes.push(action.note)
return repos
}
case 'SAVE_NOTE':
{
let repos = state.slice()
let targetRepo = _.find(repos, {key: action.repository})
if (targetRepo == null) return state
let targetNoteIndex = _.findIndex(targetRepo.notes, {key: action.note.key})
if (targetNoteIndex > -1) {
targetRepo.notes.splice(targetNoteIndex, 1, action.note)
} else {
targetRepo.notes.push(action.note)
}
return repos
}
}