1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 01:36:22 +00:00

Fixing flickering in both cases

This commit is contained in:
Max Buranbaev
2018-06-27 13:07:38 +05:00
parent 6401016424
commit 5de176757d
6 changed files with 673 additions and 383 deletions

View File

@@ -14,16 +14,27 @@ const { ipcRenderer } = require('electron')
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] const defaultEditorFontFamily = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
const buildCMRulers = (rulers, enableRulers) => const buildCMRulers = (rulers, enableRulers) =>
enableRulers ? rulers.map(ruler => ({column: ruler})) : [] (enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
export default class CodeEditor extends React.Component { export default class CodeEditor extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) this.state = { isReady: false }
this.changeHandler = (e) => this.handleChange(e) this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
leading: false,
trailing: true
})
this.changeHandler = e => this.handleChange(e)
this.focusHandler = () => { this.focusHandler = () => {
ipcRenderer.send('editor:focused', true) ipcRenderer.send('editor:focused', true)
} }
@@ -39,11 +50,15 @@ export default class CodeEditor extends React.Component {
} }
this.props.onBlur != null && this.props.onBlur(e) this.props.onBlur != null && this.props.onBlur(e)
const {storageKey, noteKey} = this.props const { storageKey, noteKey } = this.props
attachmentManagement.deleteAttachmentsNotPresentInNote(this.editor.getValue(), storageKey, noteKey) attachmentManagement.deleteAttachmentsNotPresentInNote(
this.editor.getValue(),
storageKey,
noteKey
)
} }
this.pasteHandler = (editor, e) => this.handlePaste(editor, e) this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
this.loadStyleHandler = (e) => { this.loadStyleHandler = e => {
this.editor.refresh() this.editor.refresh()
} }
this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchHandler = (e, msg) => this.handleSearch(msg)
@@ -62,7 +77,10 @@ export default class CodeEditor extends React.Component {
cm.addOverlay(component.searchState) cm.addOverlay(component.searchState)
function makeOverlay (query, style) { function makeOverlay (query, style) {
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi') query = new RegExp(
query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
'gi'
)
return { return {
token: function (stream) { token: function (stream) {
query.lastIndex = stream.pos query.lastIndex = stream.pos
@@ -94,7 +112,11 @@ export default class CodeEditor extends React.Component {
} }
] ]
if (!fs.existsSync(consts.SNIPPET_FILE)) { if (!fs.existsSync(consts.SNIPPET_FILE)) {
fs.writeFileSync(consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8') fs.writeFileSync(
consts.SNIPPET_FILE,
JSON.stringify(defaultSnippet, null, 4),
'utf8'
)
} }
this.value = this.props.value this.value = this.props.value
@@ -131,9 +153,14 @@ export default class CodeEditor extends React.Component {
cm.execCommand('insertSoftTab') cm.execCommand('insertSoftTab')
} }
cm.execCommand('goLineEnd') cm.execCommand('goLineEnd')
} else if (!charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) { } else if (
!charBeforeCursor.match(/\t|\s|\r|\n/) &&
cursor.ch > 1
) {
// text expansion on tab key if the char before is alphabet // text expansion on tab key if the char before is alphabet
const snippets = JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8')) const snippets = JSON.parse(
fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
)
if (expandSnippet(line, cursor, cm, snippets) === false) { if (expandSnippet(line, cursor, cm, snippets) === false) {
if (tabs) { if (tabs) {
cm.execCommand('insertTab') cm.execCommand('insertTab')
@@ -154,7 +181,7 @@ export default class CodeEditor extends React.Component {
// Do nothing // Do nothing
}, },
Enter: 'boostNewLineAndIndentContinueMarkdownList', Enter: 'boostNewLineAndIndentContinueMarkdownList',
'Ctrl-C': (cm) => { 'Ctrl-C': cm => {
if (cm.getOption('keyMap').substr(0, 3) === 'vim') { if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
document.execCommand('copy') document.execCommand('copy')
} }
@@ -182,10 +209,15 @@ export default class CodeEditor extends React.Component {
CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor) CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor)
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor) CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
CodeMirror.Vim.map('ZZ', ':q', 'normal') CodeMirror.Vim.map('ZZ', ':q', 'normal')
this.setState({ isReady: true })
} }
expandSnippet (line, cursor, cm, snippets) { expandSnippet (line, cursor, cm, snippets) {
const wordBeforeCursor = this.getWordBeforeCursor(line, cursor.line, cursor.ch) const wordBeforeCursor = this.getWordBeforeCursor(
line,
cursor.line,
cursor.ch
)
const templateCursorString = ':{}' const templateCursorString = ':{}'
for (let i = 0; i < snippets.length; i++) { for (let i = 0; i < snippets.length; i++) {
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) { if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
@@ -203,7 +235,10 @@ export default class CodeEditor extends React.Component {
wordBeforeCursor.range.from, wordBeforeCursor.range.from,
wordBeforeCursor.range.to wordBeforeCursor.range.to
) )
cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition }) cm.setCursor({
line: cursor.line + cursorLineNumber,
ch: cursorLinePosition
})
} }
} }
} else { } else {
@@ -245,8 +280,8 @@ export default class CodeEditor extends React.Component {
return { return {
text: wordBeforeCursor, text: wordBeforeCursor,
range: { range: {
from: {line: lineNumber, ch: originCursorPosition}, from: { line: lineNumber, ch: originCursorPosition },
to: {line: lineNumber, ch: cursorPosition} to: { line: lineNumber, ch: cursorPosition }
} }
} }
} }
@@ -268,7 +303,7 @@ export default class CodeEditor extends React.Component {
componentDidUpdate (prevProps, prevState) { componentDidUpdate (prevProps, prevState) {
let needRefresh = false let needRefresh = false
const {rulers, enableRulers} = this.props const { rulers, enableRulers } = this.props
if (prevProps.mode !== this.props.mode) { if (prevProps.mode !== this.props.mode) {
this.setMode(this.props.mode) this.setMode(this.props.mode)
} }
@@ -286,7 +321,10 @@ export default class CodeEditor extends React.Component {
needRefresh = true needRefresh = true
} }
if (prevProps.enableRulers !== enableRulers || prevProps.rulers !== rulers) { if (
prevProps.enableRulers !== enableRulers ||
prevProps.rulers !== rulers
) {
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers)) this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
} }
@@ -326,11 +364,9 @@ export default class CodeEditor extends React.Component {
} }
} }
moveCursorTo (row, col) { moveCursorTo (row, col) {}
}
scrollToLine (num) { scrollToLine (num) {}
}
focus () { focus () {
this.editor.focus() this.editor.focus()
@@ -358,8 +394,13 @@ export default class CodeEditor extends React.Component {
handleDropImage (dropEvent) { handleDropImage (dropEvent) {
dropEvent.preventDefault() dropEvent.preventDefault()
const {storageKey, noteKey} = this.props const { storageKey, noteKey } = this.props
attachmentManagement.handleAttachmentDrop(this, storageKey, noteKey, dropEvent) attachmentManagement.handleAttachmentDrop(
this,
storageKey,
noteKey,
dropEvent
)
} }
insertAttachmentMd (imageMd) { insertAttachmentMd (imageMd) {
@@ -368,34 +409,44 @@ export default class CodeEditor extends React.Component {
handlePaste (editor, e) { handlePaste (editor, e) {
const clipboardData = e.clipboardData const clipboardData = e.clipboardData
const {storageKey, noteKey} = this.props const { storageKey, noteKey } = this.props
const dataTransferItem = clipboardData.items[0] const dataTransferItem = clipboardData.items[0]
const pastedTxt = clipboardData.getData('text') const pastedTxt = clipboardData.getData('text')
const isURL = (str) => { const isURL = str => {
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
return matcher.test(str) return matcher.test(str)
} }
const isInLinkTag = (editor) => { const isInLinkTag = editor => {
const startCursor = editor.getCursor('start') const startCursor = editor.getCursor('start')
const prevChar = editor.getRange( const prevChar = editor.getRange(
{line: startCursor.line, ch: startCursor.ch - 2}, { line: startCursor.line, ch: startCursor.ch - 2 },
{line: startCursor.line, ch: startCursor.ch} { line: startCursor.line, ch: startCursor.ch }
) )
const endCursor = editor.getCursor('end') const endCursor = editor.getCursor('end')
const nextChar = editor.getRange( const nextChar = editor.getRange(
{line: endCursor.line, ch: endCursor.ch}, { line: endCursor.line, ch: endCursor.ch },
{line: endCursor.line, ch: endCursor.ch + 1} { line: endCursor.line, ch: endCursor.ch + 1 }
) )
return prevChar === '](' && nextChar === ')' return prevChar === '](' && nextChar === ')'
} }
if (dataTransferItem.type.match('image')) { if (dataTransferItem.type.match('image')) {
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem) attachmentManagement.handlePastImageEvent(
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { this,
storageKey,
noteKey,
dataTransferItem
)
} else if (
this.props.fetchUrlTitle &&
isURL(pastedTxt) &&
!isInLinkTag(editor)
) {
this.handlePasteUrl(e, editor, pastedTxt) this.handlePasteUrl(e, editor, pastedTxt)
} }
if (attachmentManagement.isAttachmentLink(pastedTxt)) { if (attachmentManagement.isAttachmentLink(pastedTxt)) {
attachmentManagement.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt) attachmentManagement
.then((modifiedText) => { .handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
.then(modifiedText => {
this.editor.replaceSelection(modifiedText) this.editor.replaceSelection(modifiedText)
}) })
e.preventDefault() e.preventDefault()
@@ -413,39 +464,49 @@ export default class CodeEditor extends React.Component {
const taggedUrl = `<${pastedTxt}>` const taggedUrl = `<${pastedTxt}>`
editor.replaceSelection(taggedUrl) editor.replaceSelection(taggedUrl)
const isImageReponse = (response) => { const isImageReponse = response => {
return response.headers.has('content-type') && return (
response.headers.has('content-type') &&
response.headers.get('content-type').match(/^image\/.+$/) response.headers.get('content-type').match(/^image\/.+$/)
)
} }
const replaceTaggedUrl = (replacement) => { const replaceTaggedUrl = replacement => {
const value = editor.getValue() const value = editor.getValue()
const cursor = editor.getCursor() const cursor = editor.getCursor()
const newValue = value.replace(taggedUrl, replacement) const newValue = value.replace(taggedUrl, replacement)
const newCursor = Object.assign({}, cursor, { ch: cursor.ch + newValue.length - value.length }) const newCursor = Object.assign({}, cursor, {
ch: cursor.ch + newValue.length - value.length
})
editor.setValue(newValue) editor.setValue(newValue)
editor.setCursor(newCursor) editor.setCursor(newCursor)
} }
fetch(pastedTxt, { fetch(pastedTxt, {
method: 'get' method: 'get'
}).then((response) => {
if (isImageReponse(response)) {
return this.mapImageResponse(response, pastedTxt)
} else {
return this.mapNormalResponse(response, pastedTxt)
}
}).then((replacement) => {
replaceTaggedUrl(replacement)
}).catch((e) => {
replaceTaggedUrl(pastedTxt)
}) })
.then(response => {
if (isImageReponse(response)) {
return this.mapImageResponse(response, pastedTxt)
} else {
return this.mapNormalResponse(response, pastedTxt)
}
})
.then(replacement => {
replaceTaggedUrl(replacement)
})
.catch(e => {
replaceTaggedUrl(pastedTxt)
})
} }
mapNormalResponse (response, pastedTxt) { mapNormalResponse (response, pastedTxt) {
return this.decodeResponse(response).then((body) => { return this.decodeResponse(response).then(body => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const parsedBody = (new window.DOMParser()).parseFromString(body, 'text/html') const parsedBody = new window.DOMParser().parseFromString(
body,
'text/html'
)
const linkWithTitle = `[${parsedBody.title}](${pastedTxt})` const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
resolve(linkWithTitle) resolve(linkWithTitle)
} catch (e) { } catch (e) {
@@ -473,10 +534,13 @@ export default class CodeEditor extends React.Component {
const _charset = headers.has('content-type') const _charset = headers.has('content-type')
? this.extractContentTypeCharset(headers.get('content-type')) ? this.extractContentTypeCharset(headers.get('content-type'))
: undefined : undefined
return response.arrayBuffer().then((buff) => { return response.arrayBuffer().then(buff => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8' const charset = _charset !== undefined &&
iconv.encodingExists(_charset)
? _charset
: 'utf-8'
resolve(iconv.decode(new Buffer(buff), charset).toString()) resolve(iconv.decode(new Buffer(buff), charset).toString())
} catch (e) { } catch (e) {
reject(e) reject(e)
@@ -486,15 +550,18 @@ export default class CodeEditor extends React.Component {
} }
extractContentTypeCharset (contentType) { extractContentTypeCharset (contentType) {
return contentType.split(';').filter((str) => { return contentType
return str.trim().toLowerCase().startsWith('charset') .split(';')
}).map((str) => { .filter(str => {
return str.replace(/['"]/g, '').split('=')[1] return str.trim().toLowerCase().startsWith('charset')
})[0] })
.map(str => {
return str.replace(/['"]/g, '').split('=')[1]
})[0]
} }
render () { render () {
const {className, fontSize} = this.props const { className, fontSize } = this.props
let fontFamily = this.props.fontFamily let fontFamily = this.props.fontFamily
fontFamily = _.isString(fontFamily) && fontFamily.length > 0 fontFamily = _.isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily) ? [fontFamily].concat(defaultEditorFontFamily)
@@ -502,18 +569,16 @@ export default class CodeEditor extends React.Component {
const width = this.props.width const width = this.props.width
return ( return (
<div <div
className={className == null className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
? 'CodeEditor'
: `CodeEditor ${className}`
}
ref='root' ref='root'
tabIndex='-1' tabIndex='-1'
style={{ style={{
fontFamily: fontFamily.join(', '), fontFamily: fontFamily.join(', '),
fontSize: fontSize, fontSize: fontSize,
width: width width: width,
opacity: this.state.isReady ? '1' : '0'
}} }}
onDrop={(e) => this.handleDropImage(e)} onDrop={e => this.handleDropImage(e)}
/> />
) )
} }

View File

@@ -24,15 +24,24 @@ const path = require('path')
const dialog = remote.dialog const dialog = remote.dialog
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = 'file://' + (process.env.NODE_ENV === 'production' const appPath =
? app.getAppPath() 'file://' +
: path.resolve()) (process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve())
const CSS_FILES = [ const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`, `${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css` `${appPath}/node_modules/codemirror/lib/codemirror.css`
] ]
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) { function buildStyle (
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
) {
return ` return `
@font-face { @font-face {
font-family: 'Lato'; font-family: 'Lato';
@@ -137,17 +146,29 @@ if (!OSX) {
defaultFontFamily.unshift('Microsoft YaHei') defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo') defaultFontFamily.unshift('meiryo')
} }
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] const defaultCodeBlockFontFamily = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
export default class MarkdownPreview extends React.Component { export default class MarkdownPreview extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = {
this.contextMenuHandler = (e) => this.handleContextMenu(e) isReady: false
this.mouseDownHandler = (e) => this.handleMouseDown(e) }
this.mouseUpHandler = (e) => this.handleMouseUp(e) this.contextMenuHandler = e => this.handleContextMenu(e)
this.DoubleClickHandler = (e) => this.handleDoubleClick(e) this.mouseDownHandler = e => this.handleMouseDown(e)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) this.mouseUpHandler = e => this.handleMouseUp(e)
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) this.DoubleClickHandler = e => this.handleDoubleClick(e)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
leading: false,
trailing: true
})
this.checkboxClickHandler = e => this.handleCheckboxClick(e)
this.saveAsTextHandler = () => this.handleSaveAsText() this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd() this.saveAsMdHandler = () => this.handleSaveAsMd()
this.saveAsHtmlHandler = () => this.handleSaveAsHtml() this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
@@ -214,31 +235,56 @@ export default class MarkdownPreview extends React.Component {
handleSaveAsHtml () { handleSaveAsHtml () {
this.exportAsDocument('html', (noteContent, exportTasks) => { this.exportAsDocument('html', (noteContent, exportTasks) => {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams() const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) const inlineStyles = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
let body = this.markdown.render(escapeHtmlCharacters(noteContent)) let body = this.markdown.render(escapeHtmlCharacters(noteContent))
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath) const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
files.forEach((file) => { files.forEach(file => {
file = file.replace('file://', '') file = file.replace('file://', '')
exportTasks.push({ exportTasks.push({
src: file, src: file,
dst: 'css' dst: 'css'
}) })
}) })
attachmentsAbsolutePaths.forEach((attachment) => { attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({ exportTasks.push({
src: attachment, src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER dst: attachmentManagement.DESTINATION_FOLDER
}) })
}) })
body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey) body = attachmentManagement.removeStorageAndNoteReferences(
body,
this.props.noteKey
)
let styles = '' let styles = ''
files.forEach((file) => { files.forEach(file => {
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">` styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
}) })
@@ -260,42 +306,51 @@ export default class MarkdownPreview extends React.Component {
exportAsDocument (fileType, contentFormatter) { exportAsDocument (fileType, contentFormatter) {
const options = { const options = {
filters: [ filters: [{ name: 'Documents', extensions: [fileType] }],
{name: 'Documents', extensions: [fileType]}
],
properties: ['openFile', 'createDirectory'] properties: ['openFile', 'createDirectory']
} }
dialog.showSaveDialog(remote.getCurrentWindow(), options, dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
(filename) => { if (filename) {
if (filename) { const content = this.props.value
const content = this.props.value const storage = this.props.storagePath
const storage = this.props.storagePath
exportNote(storage, content, filename, contentFormatter) exportNote(storage, content, filename, contentFormatter)
.then((res) => { .then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`}) dialog.showMessageBox(remote.getCurrentWindow(), {
}).catch((err) => { type: 'info',
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export') message: `Exported to ${filename}`
throw err })
}) })
} .catch(err => {
}) dialog.showErrorBox(
'Export error',
err ? err.message || err : 'Unexpected error during export'
)
throw err
})
}
})
} }
fixDecodedURI (node) { fixDecodedURI (node) {
if (node && node.children.length === 1 && typeof node.children[0] === 'string') { if (
node &&
node.children.length === 1 &&
typeof node.children[0] === 'string'
) {
const { innerText, href } = node const { innerText, href } = node
node.innerText = mdurl.decode(href) === innerText node.innerText = mdurl.decode(href) === innerText ? href : innerText
? href
: innerText
} }
} }
componentDidMount () { componentDidMount () {
this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler) this.refs.root.contentWindow.document.body.addEventListener(
'contextmenu',
this.contextMenuHandler
)
let styles = ` let styles = `
<style id='style'></style> <style id='style'></style>
@@ -303,7 +358,7 @@ export default class MarkdownPreview extends React.Component {
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
` `
CSS_FILES.forEach((file) => { CSS_FILES.forEach(file => {
styles += `<link rel="stylesheet" href="${file}">` styles += `<link rel="stylesheet" href="${file}">`
}) })
@@ -311,26 +366,66 @@ export default class MarkdownPreview extends React.Component {
this.rewriteIframe() this.rewriteIframe()
this.applyStyle() this.applyStyle()
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler) this.refs.root.contentWindow.document.addEventListener(
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler) 'mousedown',
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler) this.mouseDownHandler
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler) )
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.addEventListener(
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler) 'mouseup',
this.mouseUpHandler
)
this.refs.root.contentWindow.document.addEventListener(
'dblclick',
this.DoubleClickHandler
)
this.refs.root.contentWindow.document.addEventListener(
'drop',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.addEventListener(
'dragover',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.addEventListener(
'scroll',
this.scrollHandler
)
eventEmitter.on('export:save-text', this.saveAsTextHandler) eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
eventEmitter.on('print', this.printHandler) eventEmitter.on('print', this.printHandler)
setTimeout(() => this.setState({ isReady: true }))
} }
componentWillUnmount () { componentWillUnmount () {
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler) this.refs.root.contentWindow.document.body.removeEventListener(
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler) 'contextmenu',
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler) this.contextMenuHandler
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler) )
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.removeEventListener(
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler) 'mousedown',
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler) this.mouseDownHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'mouseup',
this.mouseUpHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'dblclick',
this.DoubleClickHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'drop',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'dragover',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'scroll',
this.scrollHandler
)
eventEmitter.off('export:save-text', this.saveAsTextHandler) eventEmitter.off('export:save-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('export:save-md', this.saveAsMdHandler)
eventEmitter.off('export:save-html', this.saveAsHtmlHandler) eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
@@ -339,14 +434,17 @@ export default class MarkdownPreview extends React.Component {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe() if (prevProps.value !== this.props.value) this.rewriteIframe()
if (prevProps.smartQuotes !== this.props.smartQuotes || if (
prevProps.sanitize !== this.props.sanitize || prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.smartArrows !== this.props.smartArrows || prevProps.sanitize !== this.props.sanitize ||
prevProps.breaks !== this.props.breaks) { prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks
) {
this.initMarkdown() this.initMarkdown()
this.rewriteIframe() this.rewriteIframe()
} }
if (prevProps.fontFamily !== this.props.fontFamily || if (
prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize || prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme || prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
@@ -355,34 +453,82 @@ export default class MarkdownPreview extends React.Component {
prevProps.theme !== this.props.theme || prevProps.theme !== this.props.theme ||
prevProps.scrollPastEnd !== this.props.scrollPastEnd || prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
prevProps.allowCustomCSS !== this.props.allowCustomCSS || prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
prevProps.customCSS !== this.props.customCSS) { prevProps.customCSS !== this.props.customCSS
) {
this.applyStyle() this.applyStyle()
this.rewriteIframe() this.rewriteIframe()
} }
} }
getStyleParams () { getStyleParams () {
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props const {
fontSize,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.props
let { fontFamily, codeBlockFontFamily } = this.props let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily) ? fontFamily
: defaultFontFamily .split(',')
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0 .map(fontName => fontName.trim())
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) .concat(defaultFontFamily)
: defaultCodeBlockFontFamily : defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} return {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
}
} }
applyStyle () { applyStyle () {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams() const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme) this.getWindow().document.getElementById(
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) 'codeTheme'
).href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
} }
GetCodeThemeLink (theme) { GetCodeThemeLink (theme) {
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default' theme = consts.THEMES.some(_theme => _theme === theme) &&
theme !== 'default'
? theme ? theme
: 'elegant' : 'elegant'
return theme.startsWith('solarized') return theme.startsWith('solarized')
@@ -391,71 +537,103 @@ export default class MarkdownPreview extends React.Component {
} }
rewriteIframe () { rewriteIframe () {
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { _.forEach(
el.removeEventListener('click', this.checkboxClickHandler) this.refs.root.contentWindow.document.querySelectorAll(
}) 'input[type="checkbox"]'
),
el => {
el.removeEventListener('click', this.checkboxClickHandler)
}
)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { _.forEach(
el.removeEventListener('click', this.linkClickHandler) this.refs.root.contentWindow.document.querySelectorAll('a'),
}) el => {
el.removeEventListener('click', this.linkClickHandler)
}
)
const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props const {
theme,
indentSize,
showCopyNotification,
storagePath,
noteKey
} = this.props
let { value, codeBlockTheme } = this.props let { value, codeBlockTheme } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g) const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g)
if (codeBlocks !== null) { if (codeBlocks !== null) {
codeBlocks.forEach((codeBlock) => { codeBlocks.forEach(codeBlock => {
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) value = value.replace(
codeBlock,
htmlTextHelper.encodeEntities(codeBlock)
)
}) })
} }
let renderedHTML = this.markdown.render(value) let renderedHTML = this.markdown.render(value)
attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey) attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey)
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath) this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
renderedHTML,
storagePath
)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { _.forEach(
el.addEventListener('click', this.checkboxClickHandler) this.refs.root.contentWindow.document.querySelectorAll(
}) 'input[type="checkbox"]'
),
el => {
el.addEventListener('click', this.checkboxClickHandler)
}
)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { _.forEach(
this.fixDecodedURI(el) this.refs.root.contentWindow.document.querySelectorAll('a'),
el.addEventListener('click', this.linkClickHandler) el => {
}) this.fixDecodedURI(el)
el.addEventListener('click', this.linkClickHandler)
}
)
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme) codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
? codeBlockTheme ? codeBlockTheme
: 'default' : 'default'
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => { _.forEach(
let syntax = CodeMirror.findModeByName(convertModeName(el.className)) this.refs.root.contentWindow.document.querySelectorAll('.code code'),
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') el => {
CodeMirror.requireMode(syntax.mode, () => { let syntax = CodeMirror.findModeByName(convertModeName(el.className))
const content = htmlTextHelper.decodeEntities(el.innerHTML) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
const copyIcon = document.createElement('i') CodeMirror.requireMode(syntax.mode, () => {
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>' const content = htmlTextHelper.decodeEntities(el.innerHTML)
copyIcon.onclick = (e) => { const copyIcon = document.createElement('i')
copy(content) copyIcon.innerHTML =
if (showCopyNotification) { '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
this.notify('Saved to Clipboard!', { copyIcon.onclick = e => {
body: 'Paste it wherever you want!', copy(content)
silent: true if (showCopyNotification) {
}) this.notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
}
} }
} el.parentNode.appendChild(copyIcon)
el.parentNode.appendChild(copyIcon) el.innerHTML = ''
el.innerHTML = '' if (codeBlockTheme.indexOf('solarized') === 0) {
if (codeBlockTheme.indexOf('solarized') === 0) { const [refThema, color] = codeBlockTheme.split(' ')
const [refThema, color] = codeBlockTheme.split(' ') el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}` } else {
} else { el.parentNode.className += ` cm-s-${codeBlockTheme}`
el.parentNode.className += ` cm-s-${codeBlockTheme}` }
} CodeMirror.runMode(content, syntax.mime, el, {
CodeMirror.runMode(content, syntax.mime, el, { tabSize: indentSize
tabSize: indentSize })
}) })
}) }
}) )
const opts = {} const opts = {}
// if (this.props.theme === 'dark') { // if (this.props.theme === 'dark') {
// opts['font-color'] = '#DDD' // opts['font-color'] = '#DDD'
@@ -463,37 +641,47 @@ export default class MarkdownPreview extends React.Component {
// opts['element-color'] = '#DDD' // opts['element-color'] = '#DDD'
// opts['fill'] = '#3A404C' // opts['fill'] = '#3A404C'
// } // }
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => { _.forEach(
Raphael.setWindow(this.getWindow()) this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
try { el => {
const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML)) Raphael.setWindow(this.getWindow())
el.innerHTML = '' try {
diagram.drawSVG(el, opts) const diagram = flowchart.parse(
_.forEach(el.querySelectorAll('a'), (el) => { htmlTextHelper.decodeEntities(el.innerHTML)
el.addEventListener('click', this.linkClickHandler) )
}) el.innerHTML = ''
} catch (e) { diagram.drawSVG(el, opts)
console.error(e) _.forEach(el.querySelectorAll('a'), el => {
el.className = 'flowchart-error' el.addEventListener('click', this.linkClickHandler)
el.innerHTML = 'Flowchart parse error: ' + e.message })
} catch (e) {
console.error(e)
el.className = 'flowchart-error'
el.innerHTML = 'Flowchart parse error: ' + e.message
}
} }
}) )
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => { _.forEach(
Raphael.setWindow(this.getWindow()) this.refs.root.contentWindow.document.querySelectorAll('.sequence'),
try { el => {
const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML)) Raphael.setWindow(this.getWindow())
el.innerHTML = '' try {
diagram.drawSVG(el, {theme: 'simple'}) const diagram = SequenceDiagram.parse(
_.forEach(el.querySelectorAll('a'), (el) => { htmlTextHelper.decodeEntities(el.innerHTML)
el.addEventListener('click', this.linkClickHandler) )
}) el.innerHTML = ''
} catch (e) { diagram.drawSVG(el, { theme: 'simple' })
console.error(e) _.forEach(el.querySelectorAll('a'), el => {
el.className = 'sequence-error' el.addEventListener('click', this.linkClickHandler)
el.innerHTML = 'Sequence diagram parse error: ' + e.message })
} catch (e) {
console.error(e)
el.className = 'sequence-error'
el.innerHTML = 'Sequence diagram parse error: ' + e.message
}
} }
}) )
} }
focus () { focus () {
@@ -505,7 +693,9 @@ export default class MarkdownPreview extends React.Component {
} }
scrollTo (targetRow) { scrollTo (targetRow) {
const blocks = this.getWindow().document.querySelectorAll('body>[data-line]') const blocks = this.getWindow().document.querySelectorAll(
'body>[data-line]'
)
for (let index = 0; index < blocks.length; index++) { for (let index = 0; index < blocks.length; index++) {
let block = blocks[index] let block = blocks[index]
@@ -525,7 +715,11 @@ export default class MarkdownPreview extends React.Component {
notify (title, options) { notify (title, options) {
if (global.process.platform === 'win32') { if (global.process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png') options.icon = path.join(
'file://',
global.__dirname,
'../../resources/app.png'
)
} }
return new window.Notification(title, options) return new window.Notification(title, options)
} }
@@ -540,7 +734,9 @@ export default class MarkdownPreview extends React.Component {
const regexNoteInternalLink = /main.html#(.+)/ const regexNoteInternalLink = /main.html#(.+)/
if (regexNoteInternalLink.test(linkHash)) { if (regexNoteInternalLink.test(linkHash)) {
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1]) const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
const targetElement = this.refs.root.contentWindow.document.getElementById(targetId) const targetElement = this.refs.root.contentWindow.document.getElementById(
targetId
)
if (targetElement != null) { if (targetElement != null) {
this.getWindow().scrollTo(0, targetElement.offsetTop) this.getWindow().scrollTo(0, targetElement.offsetTop)
@@ -574,11 +770,15 @@ export default class MarkdownPreview extends React.Component {
render () { render () {
const { className, style, tabIndex } = this.props const { className, style, tabIndex } = this.props
return ( return (
<iframe className={className != null <iframe
? 'MarkdownPreview ' + className className={
: 'MarkdownPreview' className != null ? 'MarkdownPreview ' + className : 'MarkdownPreview'
}
style={
this.state.isReady
? Object.assign(style, { opacity: '1' })
: Object.assign(style, { opacity: '0' })
} }
style={style}
tabIndex={tabIndex} tabIndex={tabIndex}
ref='root' ref='root'
/> />

View File

@@ -26,14 +26,12 @@ const TagElement = ({ tagName }) => (
* @param {Array|null} tags * @param {Array|null} tags
* @return {React.Component} * @return {React.Component}
*/ */
const TagElementList = (tags) => { const TagElementList = tags => {
if (!isArray(tags)) { if (!isArray(tags)) {
return [] return []
} }
const tagElements = tags.map(tag => ( const tagElements = tags.map(tag => TagElement({ tagName: tag }))
TagElement({tagName: tag})
))
return tagElements return tagElements
} }
@@ -59,10 +57,8 @@ const NoteItem = ({
folderName, folderName,
viewType viewType
}) => ( }) => (
<div styleName={isActive <div
? 'item--active' styleName={isActive ? 'item--active' : 'item'}
: 'item'
}
key={note.key} key={note.key}
onClick={e => handleNoteClick(e, note.key)} onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)} onContextMenu={e => handleNoteContextMenu(e, note.key)}
@@ -72,42 +68,54 @@ const NoteItem = ({
<div styleName='item-wrapper'> <div styleName='item-wrapper'>
{note.type === 'SNIPPET_NOTE' {note.type === 'SNIPPET_NOTE'
? <i styleName='item-title-icon' className='fa fa-fw fa-code' /> ? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' /> : <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
}
<div styleName='item-title'> <div styleName='item-title'>
{note.title.trim().length > 0 {note.title.trim().length > 0
? note.title ? note.title
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span> : <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
}
</div> </div>
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'> {['ALL', 'STORAGE'].includes(viewType) &&
<div styleName='item-middle-time'>{dateDisplay}</div> <div styleName='item-middle'>
<div styleName='item-middle-app-meta'> <div styleName='item-middle-time'>{dateDisplay}</div>
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'> <div styleName='item-middle-app-meta'>
{viewType === 'ALL' && storageName} <div
{viewType === 'STORAGE' && folderName} title={
viewType === 'ALL'
? storageName
: viewType === 'STORAGE' ? folderName : null
}
styleName='item-middle-app-meta-label'
>
{viewType === 'ALL' && storageName}
{viewType === 'STORAGE' && folderName}
</div>
</div> </div>
</div> </div>}
</div>}
<div styleName='item-bottom'> <div styleName='item-bottom'>
<div styleName='item-bottom-tagList'> <div styleName='item-bottom-tagList'>
{note.tags.length > 0 {note.tags.length > 0
? TagElementList(note.tags) ? TagElementList(note.tags)
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>{i18n.__('No tags')}</span> : <span
} style={{ fontStyle: 'italic', opacity: 0.5 }}
styleName='item-bottom-tagList-empty'
>
{i18n.__('No tags')}
</span>}
</div> </div>
<div> <div>
{note.isStarred {note.isStarred
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : '' ? <img
} styleName='item-star'
src='../resources/icon/icon-starred.svg'
/>
: ''}
{note.isPinned && !pathname.match(/\/starred|\/trash/) {note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : '' ? <i styleName='item-pin' className='fa fa-thumb-tack' />
} : ''}
{note.type === 'MARKDOWN_NOTE' {note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} /> ? <TodoProcess todoStatus={getTodoStatus(note.content)} />
: '' : ''}
}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -54,9 +54,8 @@ const StorageItem = ({
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
> >
{!isFolded && ( {!isFolded &&
<DraggableIcon className={styles['folderList-item-reorder']} /> <DraggableIcon className={styles['folderList-item-reorder']} />}
)}
<span <span
styleName={ styleName={
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name' isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
@@ -72,12 +71,10 @@ const StorageItem = ({
: folderName} : folderName}
</span> </span>
{!isFolded && {!isFolded &&
_.isNumber(noteCount) && ( _.isNumber(noteCount) &&
<span styleName='folderList-item-noteCount'>{noteCount}</span> <span styleName='folderList-item-noteCount'>{noteCount}</span>}
)} {isFolded &&
{isFolded && ( <span styleName='folderList-item-tooltip'>{folderName}</span>}
<span styleName='folderList-item-tooltip'>{folderName}</span>
)}
</button> </button>
) )
} }

View File

@@ -22,7 +22,6 @@ const electron = require('electron')
const { remote } = electron const { remote } = electron
class Main extends React.Component { class Main extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -60,10 +59,10 @@ class Main extends React.Component {
name: 'My Storage', name: 'My Storage',
path: path.join(remote.app.getPath('home'), 'Boostnote') path: path.join(remote.app.getPath('home'), 'Boostnote')
}) })
.then((data) => { .then(data => {
return data return data
}) })
.then((data) => { .then(data => {
if (data.storage.folders[0] != null) { if (data.storage.folders[0] != null) {
return data return data
} else { } else {
@@ -72,7 +71,7 @@ class Main extends React.Component {
color: '#1278BD', color: '#1278BD',
name: 'Default' name: 'Default'
}) })
.then((_data) => { .then(_data => {
return { return {
storage: _data.storage, storage: _data.storage,
notes: data.notes notes: data.notes
@@ -80,7 +79,7 @@ class Main extends React.Component {
}) })
} }
}) })
.then((data) => { .then(data => {
console.log(data) console.log(data)
store.dispatch({ store.dispatch({
type: 'ADD_STORAGE', type: 'ADD_STORAGE',
@@ -98,16 +97,16 @@ class Main extends React.Component {
{ {
name: 'example.html', name: 'example.html',
mode: 'html', mode: 'html',
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>' content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
}, },
{ {
name: 'example.js', name: 'example.js',
mode: 'javascript', mode: 'javascript',
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)' content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
} }
] ]
}) })
.then((note) => { .then(note => {
store.dispatch({ store.dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
@@ -120,7 +119,7 @@ class Main extends React.Component {
title: 'Welcome to Boostnote!', title: 'Welcome to Boostnote!',
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)' content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
}) })
.then((note) => { .then(note => {
store.dispatch({ store.dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
@@ -131,10 +130,10 @@ class Main extends React.Component {
.then(defaultMarkdownNote) .then(defaultMarkdownNote)
.then(() => data.storage) .then(() => data.storage)
}) })
.then((storage) => { .then(storage => {
hashHistory.push('/storages/' + storage.key) hashHistory.push('/storages/' + storage.key)
}) })
.catch((err) => { .catch(err => {
throw err throw err
}) })
} }
@@ -142,12 +141,7 @@ class Main extends React.Component {
componentDidMount () { componentDidMount () {
const { dispatch, config } = this.props const { dispatch, config } = this.props
const supportedThemes = [ const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai']
'dark',
'white',
'solarized-dark',
'monokai'
]
if (supportedThemes.indexOf(config.ui.theme) !== -1) { if (supportedThemes.indexOf(config.ui.theme) !== -1) {
document.body.setAttribute('data-theme', config.ui.theme) document.body.setAttribute('data-theme', config.ui.theme)
@@ -162,19 +156,18 @@ class Main extends React.Component {
} }
applyShortcuts() applyShortcuts()
// Reload all data // Reload all data
dataApi.init() dataApi.init().then(data => {
.then((data) => { dispatch({
dispatch({ type: 'INIT_ALL',
type: 'INIT_ALL', storages: data.storages,
storages: data.storages, notes: data.notes
notes: data.notes
})
if (data.storages.length < 1) {
this.init()
}
}) })
if (data.storages.length < 1) {
this.init()
}
})
eventEmitter.on('editor:fullscreen', this.toggleFullScreen) eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
} }
@@ -199,34 +192,40 @@ class Main extends React.Component {
handleMouseUp (e) { handleMouseUp (e) {
// Change width of NoteList component. // Change width of NoteList component.
if (this.state.isRightSliderFocused) { if (this.state.isRightSliderFocused) {
this.setState({ this.setState(
isRightSliderFocused: false {
}, () => { isRightSliderFocused: false
const { dispatch } = this.props },
const newListWidth = this.state.listWidth () => {
// TODO: ConfigManager should dispatch itself. const { dispatch } = this.props
ConfigManager.set({listWidth: newListWidth}) const newListWidth = this.state.listWidth
dispatch({ // TODO: ConfigManager should dispatch itself.
type: 'SET_LIST_WIDTH', ConfigManager.set({ listWidth: newListWidth })
listWidth: newListWidth dispatch({
}) type: 'SET_LIST_WIDTH',
}) listWidth: newListWidth
})
}
)
} }
// Change width of SideNav component. // Change width of SideNav component.
if (this.state.isLeftSliderFocused) { if (this.state.isLeftSliderFocused) {
this.setState({ this.setState(
isLeftSliderFocused: false {
}, () => { isLeftSliderFocused: false
const { dispatch } = this.props },
const navWidth = this.state.navWidth () => {
// TODO: ConfigManager should dispatch itself. const { dispatch } = this.props
ConfigManager.set({ navWidth }) const navWidth = this.state.navWidth
dispatch({ // TODO: ConfigManager should dispatch itself.
type: 'SET_NAV_WIDTH', ConfigManager.set({ navWidth })
navWidth dispatch({
}) type: 'SET_NAV_WIDTH',
}) navWidth
})
}
)
} }
} }
@@ -271,8 +270,8 @@ class Main extends React.Component {
} }
hideLeftLists (noteDetail, noteList, mainBody) { hideLeftLists (noteDetail, noteList, mainBody) {
this.setState({noteDetailWidth: noteDetail.style.left}) this.setState({ noteDetailWidth: noteDetail.style.left })
this.setState({mainBodyWidth: mainBody.style.left}) this.setState({ mainBodyWidth: mainBody.style.left })
noteDetail.style.left = '0px' noteDetail.style.left = '0px'
mainBody.style.left = '0px' mainBody.style.left = '0px'
noteList.style.display = 'none' noteList.style.display = 'none'
@@ -294,33 +293,36 @@ class Main extends React.Component {
<div <div
className='Main' className='Main'
styleName='root' styleName='root'
onMouseMove={(e) => this.handleMouseMove(e)} onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={(e) => this.handleMouseUp(e)} onMouseUp={e => this.handleMouseUp(e)}
> >
<SideNav <SideNav
{..._.pick(this.props, [ {..._.pick(this.props, ['dispatch', 'data', 'config', 'location'])}
'dispatch',
'data',
'config',
'location'
])}
width={this.state.navWidth} width={this.state.navWidth}
/> />
{!config.isSideNavFolded && {!config.isSideNavFolded &&
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'} <div
style={{left: this.state.navWidth}} styleName={
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)} this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
}
style={{ left: this.state.navWidth }}
onMouseDown={e => this.handleLeftSlideMouseDown(e)}
draggable='false' draggable='false'
> >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div> </div>}
} <div
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'} styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
id='main-body' id='main-body'
ref='body' ref='body'
style={{left: config.isSideNavFolded ? foldedNavigationWidth : this.state.navWidth}} style={{
left: config.isSideNavFolded
? foldedNavigationWidth
: this.state.navWidth
}}
> >
<TopBar style={{width: this.state.listWidth}} <TopBar
style={{ width: this.state.listWidth }}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'config', 'config',
@@ -329,7 +331,8 @@ class Main extends React.Component {
'location' 'location'
])} ])}
/> />
<NoteList style={{width: this.state.listWidth}} <NoteList
style={{ width: this.state.listWidth }}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'data', 'data',
@@ -338,15 +341,20 @@ class Main extends React.Component {
'location' 'location'
])} ])}
/> />
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'} <div
style={{left: this.state.listWidth - 1}} styleName={
onMouseDown={(e) => this.handleRightSlideMouseDown(e)} this.state.isRightSliderFocused
? 'slider-right--active'
: 'slider-right'
}
style={{ left: this.state.listWidth - 1 }}
onMouseDown={e => this.handleRightSlideMouseDown(e)}
draggable='false' draggable='false'
> >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div> </div>
<Detail <Detail
style={{left: this.state.listWidth}} style={{ left: this.state.listWidth }}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'data', 'data',
@@ -374,4 +382,4 @@ Main.propTypes = {
data: PropTypes.shape({}).isRequired data: PropTypes.shape({}).isRequired
} }
export default connect((x) => x)(CSSModules(Main, styles)) export default connect(x => x)(CSSModules(Main, styles))

View File

@@ -1,72 +1,83 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<link rel="stylesheet" href="../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8"> <link rel="stylesheet" href="../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8">
<link rel="shortcut icon" href="../resources/favicon.ico"> <link rel="shortcut icon" href="../resources/favicon.ico">
<link rel="stylesheet" href="../node_modules/codemirror/lib/codemirror.css"> <link rel="stylesheet" href="../node_modules/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="../node_modules/katex/dist/katex.min.css">
<link rel="stylesheet" href="../node_modules/codemirror/addon/dialog/dialog.css"> <link rel="stylesheet" href="../node_modules/codemirror/addon/dialog/dialog.css">
<title>Boostnote</title> <title>Boostnote</title>
<style> <style>
@font-face { @font-face {
font-family: 'OpenSans'; font-family: 'OpenSans';
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */ 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.woff') format('woff'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.ttf') format('truetype'); url('../resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
@font-face {
font-family: 'Lato'; @font-face {
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */ font-family: 'Lato';
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */ src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.ttf') format('truetype'); url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
font-style: normal; url('../resources/fonts/Lato-Regular.ttf') format('truetype');
font-weight: normal; font-style: normal;
text-rendering: optimizeLegibility; font-weight: normal;
} text-rendering: optimizeLegibility;
#loadingCover{ }
background-color: #f4f4f4;
position: absolute; #loadingCover {
top: 0; background-color: #f4f4f4;
bottom: 0; position: absolute;
left: 0; top: 0;
right: 0; bottom: 0;
box-sizing: border-box; left: 0;
padding: 65px 0; right: 0;
font-family: sans-serif; box-sizing: border-box;
} padding: 65px 0;
#loadingCover img{ font-family: sans-serif;
display: block; }
margin: 75px auto 5px;
width: 160px; #loadingCover img {
height: 160px; display: block;
} margin: 75px auto 5px;
#loadingCover .message{ width: 160px;
font-size: 30px; height: 160px;
text-align: center; }
line-height: 1.6;
font-weight: 100; #loadingCover .message {
color: #888; font-size: 30px;
} text-align: center;
.CodeEditor { line-height: 1.6;
opacity: 1 !important; font-weight: 100;
pointer-events: auto !important; color: #888;
} }
.CodeMirror-ruler {
border-left-color: rgba(142, 142, 142, 0.5); .CodeEditor {
mix-blend-mode: difference; opacity: 1 !important;
} pointer-events: auto !important;
}
.CodeMirror-ruler {
border-left-color: rgba(142, 142, 142, 0.5);
mix-blend-mode: difference;
}
</style> </style>
</head> </head>
<body> <body>
<div id="loadingCover"> <div id="loadingCover">
<img src="../resources/app.png"> <img src="../resources/app.png">
<div class='message'><i class="fa fa-spinner fa-spin" spin></i></div> <div class='message'>
<i class="fa fa-spinner fa-spin" spin></i>
</div>
</div> </div>
<div id="content"></div> <div id="content"></div>
@@ -130,4 +141,5 @@
} }
</style> </style>
</body> </body>
</html> </html>