diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index 8ecf1851..b837c206 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -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 (
)
@@ -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,
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
new file mode 100644
index 00000000..c2e359d8
--- /dev/null
+++ b/browser/components/MarkdownEditor.js
@@ -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 (
+ this.handleContextMenu(e)}
+ >
+ this.handleChange(e)}
+ />
+ this.handleContextMenu(e)}
+ tabIndex='0'
+ value={value}
+ />
+
+ )
+ }
+}
+
+MarkdownEditor.propTypes = {
+ className: PropTypes.string,
+ value: PropTypes.string,
+ onChange: PropTypes.func,
+ ignorePreviewPointerEvents: PropTypes.bool
+}
+
+export default CSSModules(MarkdownEditor, styles)
diff --git a/browser/components/MarkdownEditor.styl b/browser/components/MarkdownEditor.styl
new file mode 100644
index 00000000..d62114ac
--- /dev/null
+++ b/browser/components/MarkdownEditor.styl
@@ -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
+
diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js
index f4c00971..edcbadce 100644
--- a/browser/components/MarkdownPreview.js
+++ b/browser/components/MarkdownPreview.js
@@ -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(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/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 = `
+
+
+
+ `
+ 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 (
- 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'
- }}
+
)
}
@@ -221,5 +88,5 @@ MarkdownPreview.propTypes = {
onMouseDown: PropTypes.func,
onMouseMove: PropTypes.func,
className: PropTypes.string,
- content: PropTypes.string
+ value: PropTypes.string
}
diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl
new file mode 100644
index 00000000..70eb65b0
--- /dev/null
+++ b/browser/components/markdown.styl
@@ -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
diff --git a/browser/lib/Repository.js b/browser/lib/Repository.js
index b23e497d..a3c0edc2 100644
--- a/browser/lib/Repository.js
+++ b/browser/lib/Repository.js
@@ -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)
})
})
}
diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js
index d8636f8e..9667bf80 100644
--- a/browser/lib/markdown.js
+++ b/browser/lib/markdown.js
@@ -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 `
${str}`
+ let output = ''
+ try {
+ output = katex.renderToString(str.trim())
+ } catch (err) {
+ output = `
${err.message}`
+ }
+ return output
},
blockRenderer: function (str) {
- return `
${str}
`
+ let output = ''
+ try {
+ output = katex.renderToString(str.trim(), {displayMode: true})
+ } catch (err) {
+ output = `
${err.message}
`
+ }
+ return output
}
})
md.use(require('markdown-it-checkbox'))
diff --git a/browser/main/NoteDetail/NoteDetail.styl b/browser/main/Detail/Detail.styl
similarity index 100%
rename from browser/main/NoteDetail/NoteDetail.styl
rename to browser/main/Detail/Detail.styl
index 3a641159..8d2374ce 100644
--- a/browser/main/NoteDetail/NoteDetail.styl
+++ b/browser/main/Detail/Detail.styl
@@ -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
-
diff --git a/browser/main/Detail/NoteDetail.js b/browser/main/Detail/NoteDetail.js
new file mode 100644
index 00000000..b0b55f7d
--- /dev/null
+++ b/browser/main/Detail/NoteDetail.js
@@ -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 (
+
+
+
+
FOLDER SELECT
+
TAG SELECT
+
+
+
+
+
+
+
+
+ this.handleChange(e)}
+ ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
+ />
+
+
+ )
+ }
+}
+
+NoteDetail.propTypes = {
+ dispatch: PropTypes.func,
+ repositories: PropTypes.array,
+ style: PropTypes.shape({
+ left: PropTypes.number
+ }),
+ ignorePreviewPointerEvents: PropTypes.bool
+}
+
+export default CSSModules(NoteDetail, styles)
diff --git a/browser/main/Detail/NoteDetail.styl b/browser/main/Detail/NoteDetail.styl
new file mode 100644
index 00000000..8cd7235a
--- /dev/null
+++ b/browser/main/Detail/NoteDetail.styl
@@ -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
diff --git a/browser/main/Detail/index.js b/browser/main/Detail/index.js
new file mode 100644
index 00000000..e536b857
--- /dev/null
+++ b/browser/main/Detail/index.js
@@ -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 (
+
+
+
{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N
to create a new post
+
+
+ )
+ }
+
+ return (
+
+ )
+ }
+}
+
+Detail.propTypes = {
+ dispatch: PropTypes.func,
+ repositories: PropTypes.array,
+ style: PropTypes.shape({
+ left: PropTypes.number
+ }),
+ ignorePreviewPointerEvents: PropTypes.bool
+}
+
+export default CSSModules(Detail, styles)
diff --git a/browser/main/Main.js b/browser/main/Main.js
index c3489f87..c0aa045f 100644
--- a/browser/main/Main.js
+++ b/browser/main/Main.js
@@ -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 {
>
-
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 (
- this.handleRightClick(e)}
- onMouseUp={e => this.handleMouseUp(e)}
- onMouseMove={e => this.handleMouseMove(e)}
- onMouseDown={e => this.handleMouseDowm(e)}
- className='ArticleEditor'
- >
- {showPreview
- ?
- :
this.handleBlurCodeEditor(e)}
- onChange={value => this.handleCodeEditorChange(value)}
- article={article}
- />
- }
- {article.mode === 'markdown'
- ?
- : null
- }
-
- )
- }
-}
-
-ArticleEditor.propTypes = {
- article: PropTypes.shape({
- content: PropTypes.string,
- key: PropTypes.string,
- mode: PropTypes.string
- }),
- onChange: PropTypes.func,
- parent: PropTypes.object
-}
diff --git a/browser/main/NoteDetail/index.js b/browser/main/NoteDetail/index.js
deleted file mode 100644
index 3d9b7c56..00000000
--- a/browser/main/NoteDetail/index.js
+++ /dev/null
@@ -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 (
-
-
{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N
to create a new post
-
- )
- }
-
- render () {
- let isEmpty = true
- let view = isEmpty
- ? this.renderEmpty()
- : null
- return (
-
- {view}
-
- )
- }
-}
-
-NoteDetail.propTypes = {
- dispatch: PropTypes.func,
- repositories: PropTypes.array,
- style: PropTypes.shape({
- left: PropTypes.number
- })
-}
-
-export default CSSModules(NoteDetail, styles)
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index a4896d47..51993214 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -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 {tag}
- })
- let key = `${note._repository.key}-${note.key}`
- let isActive = location.query.key === key
- return (
- this.handleNoteClick(key)(e)}
- >
-
-
+ let noteElements = notes
+ .map((note) => {
+ let folder = _.find(note._repository.folders, {key: note.folder})
+ let tagElements = note.tags.map((tag) => {
+ return
{tag}
+ })
+ let key = `${note._repository.key}-${note.key}`
+ let isActive = location.query.key === key
+ return (
+
this.handleNoteClick(key)(e)}
+ >
+
+
+
+
+ {folder.name}
+
+
+
+ {moment(note.createdAt).fromNow()}
+
-
- {folder.name}
-
- {moment(note.createdAt).fromNow()}
-
+
{note.title}
+
+
{tagElements.length > 0 ? tagElements : Not tagged yet}
-
-
{note.title}
-
-
{tagElements.length > 0 ? tagElements : Not tagged yet}
-
-
- )
- })
+ )
+ })
return (
{
+ 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
+}
diff --git a/browser/main/store.js b/browser/main/store.js
index 3b315308..df0678b0 100644
--- a/browser/main/store.js
+++ b/browser/main/store.js
@@ -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
}
}