mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8658abbad | ||
|
|
0387b33f6c | ||
|
|
41042fe64e | ||
|
|
1efc3bb15d | ||
|
|
0816a9410b | ||
|
|
cb823eaaa6 | ||
|
|
44f027bf18 | ||
|
|
571d159a1f | ||
|
|
a30d977727 | ||
|
|
da9067a672 | ||
|
|
5ed5950ed5 | ||
|
|
f53c182cfb | ||
|
|
bb2978b370 | ||
|
|
66ae4a5163 | ||
|
|
f435595ad8 | ||
|
|
4bec4596fe | ||
|
|
10b86aec49 | ||
|
|
f3cabfcb4d | ||
|
|
46fc010cf9 | ||
|
|
e370c76521 | ||
|
|
1c283f88d0 | ||
|
|
cfffa1b10a | ||
|
|
8aaae4de49 | ||
|
|
8fb0b5b572 | ||
|
|
12262671ee | ||
|
|
3165c62985 | ||
|
|
1ca1166f74 | ||
|
|
7b4d2f3b97 | ||
|
|
ccce75a047 | ||
|
|
410e82e375 | ||
|
|
bd385ec062 | ||
|
|
638227ef25 | ||
|
|
e70b35126b | ||
|
|
d01a7b16a1 | ||
|
|
326d873279 | ||
|
|
74c32bed29 | ||
|
|
c633ba9497 | ||
|
|
fa1ab04409 | ||
|
|
f370508c93 | ||
|
|
10f5ad6127 | ||
|
|
4800a88cf6 | ||
|
|
9d8bc40594 | ||
|
|
7d3d96a6e0 | ||
|
|
4c99429d76 | ||
|
|
96fbd7572c | ||
|
|
b012bba6d6 | ||
|
|
45d90b2530 | ||
|
|
a8b946a07a | ||
|
|
18392c4f23 | ||
|
|
88e7869284 | ||
|
|
3c8332c448 | ||
|
|
207f95693a | ||
|
|
de15120bf8 | ||
|
|
1dcf11e1c4 | ||
|
|
b74ba22c44 | ||
|
|
fa2d34dcfc | ||
|
|
0280a5f09e | ||
|
|
c22b7f81c1 | ||
|
|
9344fd78d8 | ||
|
|
e89c0a3e61 | ||
|
|
5aa7ef5738 | ||
|
|
88ac6c6fc6 | ||
|
|
a537f9762b | ||
|
|
aab192223a | ||
|
|
6439810d03 | ||
|
|
c5c78763d1 | ||
|
|
012908e32d | ||
|
|
1f1e545538 | ||
|
|
5b8519b2b8 | ||
|
|
6502158716 | ||
|
|
7c525ab7a6 | ||
|
|
778e3d9799 | ||
|
|
e5f986a959 | ||
|
|
d76ecd5423 | ||
|
|
6bcb6398f8 | ||
|
|
6c341d8fa5 | ||
|
|
0d5e57eeab | ||
|
|
aef603ed8c | ||
|
|
97600e526b | ||
|
|
f3f6095d81 | ||
|
|
fb7280127c | ||
|
|
60df509fc6 | ||
|
|
ced237fbda | ||
|
|
9473a26892 | ||
|
|
e5825e898d | ||
|
|
8d59bd9f71 | ||
|
|
8f90bb83e7 | ||
|
|
ba888a7165 | ||
|
|
9591a7cac2 | ||
|
|
e8fd53d8b7 | ||
|
|
3b7c36b1c9 | ||
|
|
2750a3ef30 | ||
|
|
7d59f51244 | ||
|
|
77542597f5 | ||
|
|
2a26e8a010 | ||
|
|
8b4c1a6c38 | ||
|
|
7d7e277cc6 | ||
|
|
cb1da609a4 | ||
|
|
e4fbdb35a3 | ||
|
|
c3586372e0 | ||
|
|
808d193543 | ||
|
|
0b65b70af2 | ||
|
|
deab34abd6 | ||
|
|
646cded650 | ||
|
|
9047320301 | ||
|
|
f16b054f01 | ||
|
|
fea856202d | ||
|
|
4f15cc3f08 | ||
|
|
3b524f6aba | ||
|
|
051ccad29a | ||
|
|
c82eba05d1 | ||
|
|
74af199afc | ||
|
|
c2afdba659 | ||
|
|
e6ae45f133 | ||
|
|
129ef6766b | ||
|
|
3194a7b1a2 | ||
|
|
46a733ba5b | ||
|
|
466844fc55 | ||
|
|
0cc793e3fe | ||
|
|
0fdfb385a4 | ||
|
|
419a4c6b2d | ||
|
|
55e9441547 | ||
|
|
7abff6ded4 | ||
|
|
a648d310a5 | ||
|
|
4b56fa56b5 | ||
|
|
18bf700936 | ||
|
|
67ddff736c | ||
|
|
1bba125a2a | ||
|
|
187acad592 | ||
|
|
5e07c7b3e1 | ||
|
|
cd942cf8b6 | ||
|
|
c43589fe6a | ||
|
|
d4594eff3b | ||
|
|
809a0e846b | ||
|
|
7f00ce2598 | ||
|
|
d7d77dbfe9 | ||
|
|
7a124c74cc | ||
|
|
f8549f4643 | ||
|
|
0476ff70da | ||
|
|
5ec541c3c1 | ||
|
|
7b920348f3 | ||
|
|
51b1ef41a1 | ||
|
|
12447effc9 | ||
|
|
dc1b059a9d | ||
|
|
a676ebf46c | ||
|
|
338f9fb5a9 | ||
|
|
98c9132d4a | ||
|
|
a054f12492 | ||
|
|
c42ac1df1b | ||
|
|
d9d46cda1c | ||
|
|
f678a17505 | ||
|
|
3da4bb69ce | ||
|
|
3e2b876c59 | ||
|
|
10daf2599d | ||
|
|
56d6eb7077 | ||
|
|
9b54272f8e | ||
|
|
685e003db8 | ||
|
|
701fc0bc80 | ||
|
|
e5518ac0fa | ||
|
|
8e1bf48cd1 | ||
|
|
8dd82e1a3b | ||
|
|
f04ad1e702 | ||
|
|
dc03bb76e6 | ||
|
|
d894bb4aab | ||
|
|
83da07a941 |
2
LICENSE
2
LICENSE
@@ -2,7 +2,7 @@ GPL-3.0
|
||||
|
||||
Boostnote - an open source note-taking app made for programmers just like you.
|
||||
|
||||
Copyright (C) 2017 Maisin&Co., Inc.
|
||||
Copyright (C) 2017 - 2018 BoostIO
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -7,6 +7,9 @@ import path from 'path'
|
||||
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import fs from 'fs'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import iconv from 'iconv-lite'
|
||||
const { ipcRenderer } = require('electron')
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
@@ -31,8 +34,13 @@ export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.changeHandler = (e) => this.handleChange(e)
|
||||
this.focusHandler = () => {
|
||||
ipcRenderer.send('editor:focused', true)
|
||||
}
|
||||
this.blurHandler = (editor, e) => {
|
||||
ipcRenderer.send('editor:focused', false)
|
||||
if (e == null) return null
|
||||
let el = e.relatedTarget
|
||||
while (el != null) {
|
||||
@@ -47,6 +55,39 @@ export default class CodeEditor extends React.Component {
|
||||
this.loadStyleHandler = (e) => {
|
||||
this.editor.refresh()
|
||||
}
|
||||
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||
this.searchState = null
|
||||
}
|
||||
|
||||
handleSearch (msg) {
|
||||
const cm = this.editor
|
||||
const component = this
|
||||
|
||||
if (component.searchState) cm.removeOverlay(component.searchState)
|
||||
if (msg.length < 3) return
|
||||
|
||||
cm.operation(function () {
|
||||
component.searchState = makeOverlay(msg, 'searching')
|
||||
cm.addOverlay(component.searchState)
|
||||
|
||||
function makeOverlay (query, style) {
|
||||
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi')
|
||||
return {
|
||||
token: function (stream) {
|
||||
query.lastIndex = stream.pos
|
||||
var match = query.exec(stream.string)
|
||||
if (match && match.index === stream.pos) {
|
||||
stream.pos += match[0].length || 1
|
||||
return style
|
||||
} else if (match) {
|
||||
stream.pos = match.index
|
||||
} else {
|
||||
stream.skipToEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -92,7 +133,7 @@ export default class CodeEditor extends React.Component {
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': (cm) => {
|
||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
document.execCommand('copy')
|
||||
@@ -104,9 +145,14 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
this.setMode(this.props.mode)
|
||||
|
||||
this.editor.on('focus', this.focusHandler)
|
||||
this.editor.on('blur', this.blurHandler)
|
||||
this.editor.on('change', this.changeHandler)
|
||||
this.editor.on('paste', this.pasteHandler)
|
||||
eventEmitter.on('top:search', this.searchHandler)
|
||||
|
||||
eventEmitter.emit('code:init')
|
||||
this.editor.on('scroll', this.scrollHandler)
|
||||
|
||||
const editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||
@@ -123,9 +169,12 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.editor.off('focus', this.focusHandler)
|
||||
this.editor.off('blur', this.blurHandler)
|
||||
this.editor.off('change', this.changeHandler)
|
||||
this.editor.off('paste', this.pasteHandler)
|
||||
eventEmitter.off('top:search', this.searchHandler)
|
||||
this.editor.off('scroll', this.scrollHandler)
|
||||
const editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||
}
|
||||
@@ -231,27 +280,92 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
handlePaste (editor, e) {
|
||||
const dataTransferItem = e.clipboardData.items[0]
|
||||
if (!dataTransferItem.type.match('image')) return
|
||||
|
||||
const blob = dataTransferItem.getAsFile()
|
||||
const reader = new window.FileReader()
|
||||
let base64data
|
||||
|
||||
reader.readAsDataURL(blob)
|
||||
reader.onloadend = () => {
|
||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||
base64data += base64data.replace('+', ' ')
|
||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||
const imageName = Math.random().toString(36).slice(-16)
|
||||
const storagePath = findStorage(this.props.storageKey).path
|
||||
const imageDir = path.join(storagePath, 'images')
|
||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||
const imagePath = path.join(imageDir, `${imageName}.png`)
|
||||
fs.writeFile(imagePath, binaryData, 'binary')
|
||||
const imageMd = `})`
|
||||
this.insertImageMd(imageMd)
|
||||
const clipboardData = e.clipboardData
|
||||
const dataTransferItem = clipboardData.items[0]
|
||||
const pastedTxt = clipboardData.getData('text')
|
||||
const isURL = (str) => {
|
||||
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||
return matcher.test(str)
|
||||
}
|
||||
if (dataTransferItem.type.match('image')) {
|
||||
const blob = dataTransferItem.getAsFile()
|
||||
const reader = new FileReader()
|
||||
let base64data
|
||||
|
||||
reader.readAsDataURL(blob)
|
||||
reader.onloadend = () => {
|
||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||
base64data += base64data.replace('+', ' ')
|
||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||
const imageName = Math.random().toString(36).slice(-16)
|
||||
const storagePath = findStorage(this.props.storageKey).path
|
||||
const imageDir = path.join(storagePath, 'images')
|
||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||
const imagePath = path.join(imageDir, `${imageName}.png`)
|
||||
fs.writeFile(imagePath, binaryData, 'binary')
|
||||
const imageMd = `})`
|
||||
this.insertImageMd(imageMd)
|
||||
}
|
||||
} else if (this.props.fetchUrlTitle && isURL(pastedTxt)) {
|
||||
this.handlePasteUrl(e, editor, pastedTxt)
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(e)
|
||||
}
|
||||
}
|
||||
|
||||
handlePasteUrl (e, editor, pastedTxt) {
|
||||
e.preventDefault()
|
||||
const taggedUrl = `<${pastedTxt}>`
|
||||
editor.replaceSelection(taggedUrl)
|
||||
|
||||
fetch(pastedTxt, {
|
||||
method: 'get'
|
||||
}).then((response) => {
|
||||
return this.decodeResponse(response)
|
||||
}).then((response) => {
|
||||
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html')
|
||||
const value = editor.getValue()
|
||||
const cursor = editor.getCursor()
|
||||
const LinkWithTitle = `[${parsedResponse.title}](${pastedTxt})`
|
||||
const newValue = value.replace(taggedUrl, LinkWithTitle)
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(cursor)
|
||||
}).catch((e) => {
|
||||
const value = editor.getValue()
|
||||
const newValue = value.replace(taggedUrl, pastedTxt)
|
||||
const cursor = editor.getCursor()
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(cursor)
|
||||
})
|
||||
}
|
||||
|
||||
decodeResponse (response) {
|
||||
const headers = response.headers
|
||||
const _charset = headers.has('content-type')
|
||||
? this.extractContentTypeCharset(headers.get('content-type'))
|
||||
: undefined
|
||||
return response.arrayBuffer().then((buff) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8'
|
||||
resolve(iconv.decode(new Buffer(buff), charset).toString())
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
extractContentTypeCharset (contentType) {
|
||||
return contentType.split(';').filter((str) => {
|
||||
return str.trim().toLowerCase().startsWith('charset')
|
||||
}).map((str) => {
|
||||
return str.replace(/['"]/g, '').split('=')[1]
|
||||
})[0]
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
@@ -5,7 +5,7 @@ import styles from './MarkdownEditor.styl'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import {findStorage} from 'browser/lib/findStorage'
|
||||
|
||||
class MarkdownEditor extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -92,7 +92,9 @@ class MarkdownEditor extends React.Component {
|
||||
if (this.state.isLocked) return
|
||||
this.setState({ keyPressed: new Set() })
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'BLUR') {
|
||||
if (config.editor.switchPreview === 'BLUR' ||
|
||||
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
|
||||
) {
|
||||
const cursorPosition = this.refs.code.editor.getCursor()
|
||||
this.setState({
|
||||
status: 'PREVIEW'
|
||||
@@ -104,6 +106,20 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleDoubleClick (e) {
|
||||
if (this.state.isLocked) return
|
||||
this.setState({keyPressed: new Set()})
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'DBL_CLICK') {
|
||||
this.setState({
|
||||
status: 'CODE'
|
||||
}, () => {
|
||||
this.refs.code.focus()
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handlePreviewMouseDown (e) {
|
||||
this.previewMouseDownedAt = new Date()
|
||||
}
|
||||
@@ -245,6 +261,7 @@ class MarkdownEditor extends React.Component {
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
storageKey={storageKey}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
/>
|
||||
@@ -262,8 +279,10 @@ class MarkdownEditor extends React.Component {
|
||||
lineNumber={config.preview.lineNumber}
|
||||
indentSize={editorIndentSize}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
ref='preview'
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||
tabIndex='0'
|
||||
value={this.state.renderValue}
|
||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||
|
||||
156
browser/components/MarkdownPreview.js
Normal file → Executable file
156
browser/components/MarkdownPreview.js
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import Markdown from 'browser/lib/markdown'
|
||||
import _ from 'lodash'
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
@@ -9,10 +9,10 @@ import Raphael from 'raphael'
|
||||
import flowchart from 'flowchart'
|
||||
import SequenceDiagram from 'js-sequence-diagrams'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import fs from 'fs'
|
||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import mdurl from 'mdurl'
|
||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { app } = remote
|
||||
@@ -23,6 +23,10 @@ const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
||||
? app.getAppPath()
|
||||
: path.resolve())
|
||||
const CSS_FILES = [
|
||||
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||
]
|
||||
|
||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd) {
|
||||
return `
|
||||
@@ -116,6 +120,8 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
@@ -124,6 +130,13 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.printHandler = () => this.handlePrint()
|
||||
|
||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
||||
this.initMarkdown = this.initMarkdown.bind(this)
|
||||
this.initMarkdown()
|
||||
}
|
||||
|
||||
initMarkdown () {
|
||||
const { smartQuotes } = this.props
|
||||
this.markdown = new Markdown({ typographer: smartQuotes })
|
||||
}
|
||||
|
||||
handlePreviewAnchorClick (e) {
|
||||
@@ -146,13 +159,21 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.props.onCheckboxClick(e)
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
if (!this.props.onContextMenu) return
|
||||
this.props.onContextMenu(e)
|
||||
}
|
||||
|
||||
handleDoubleClick (e) {
|
||||
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
if (!this.props.onMouseDown) return
|
||||
if (e.target != null) {
|
||||
switch (e.target.tagName) {
|
||||
case 'A':
|
||||
@@ -180,8 +201,35 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleSaveAsHtml () {
|
||||
this.exportAsDocument('html', (value) => {
|
||||
return this.refs.root.contentWindow.document.documentElement.outerHTML
|
||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
||||
const body = this.markdown.render(noteContent)
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
|
||||
files.forEach((file) => {
|
||||
file = file.replace('file://', '')
|
||||
exportTasks.push({
|
||||
src: file,
|
||||
dst: 'css'
|
||||
})
|
||||
})
|
||||
|
||||
let styles = ''
|
||||
files.forEach((file) => {
|
||||
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||
})
|
||||
|
||||
return `<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||
<style id="style">${inlineStyles}</style>
|
||||
${styles}
|
||||
</head>
|
||||
<body>${body}</body>
|
||||
</html>`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -189,23 +237,29 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.refs.root.contentWindow.print()
|
||||
}
|
||||
|
||||
exportAsDocument (fileType, formatter) {
|
||||
exportAsDocument (fileType, contentFormatter) {
|
||||
const options = {
|
||||
filters: [
|
||||
{ name: 'Documents', extensions: [fileType] }
|
||||
{name: 'Documents', extensions: [fileType]}
|
||||
],
|
||||
properties: ['openFile', 'createDirectory']
|
||||
}
|
||||
const value = formatter ? formatter.call(this, this.props.value) : this.props.value
|
||||
|
||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
||||
(filename) => {
|
||||
if (filename) {
|
||||
fs.writeFile(filename, value, (err) => {
|
||||
if (err) throw err
|
||||
(filename) => {
|
||||
if (filename) {
|
||||
const content = this.props.value
|
||||
const storage = this.props.storagePath
|
||||
|
||||
exportNote(storage, content, filename, contentFormatter)
|
||||
.then((res) => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
||||
}).catch((err) => {
|
||||
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fixDecodedURI (node) {
|
||||
@@ -222,20 +276,26 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
||||
|
||||
this.refs.root.contentWindow.document.head.innerHTML = `
|
||||
let styles = `
|
||||
<style id='style'></style>
|
||||
<link rel="stylesheet" href="${appPath}/node_modules/katex/dist/katex.min.css">
|
||||
<link rel="stylesheet" href="${appPath}/node_modules/codemirror/lib/codemirror.css">
|
||||
<link rel="stylesheet" id="codeTheme">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
`
|
||||
|
||||
CSS_FILES.forEach((file) => {
|
||||
styles += `<link rel="stylesheet" href="${file}">`
|
||||
})
|
||||
|
||||
this.refs.root.contentWindow.document.head.innerHTML = styles
|
||||
this.rewriteIframe()
|
||||
this.applyStyle()
|
||||
|
||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('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-md', this.saveAsMdHandler)
|
||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||
@@ -246,8 +306,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('mousedown', 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-md', this.saveAsMdHandler)
|
||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||
@@ -256,6 +318,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||
if (prevProps.smartQuotes !== this.props.smartQuotes) {
|
||||
this.initMarkdown()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
if (prevProps.fontFamily !== this.props.fontFamily ||
|
||||
prevProps.fontSize !== this.props.fontSize ||
|
||||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||
@@ -269,25 +335,31 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
applyStyle () {
|
||||
getStyleParams () {
|
||||
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props
|
||||
let { fontFamily, codeBlockFontFamily } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
|
||||
this.setCodeTheme(codeBlockTheme)
|
||||
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd}
|
||||
}
|
||||
|
||||
applyStyle () {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd} = this.getStyleParams()
|
||||
|
||||
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd)
|
||||
}
|
||||
|
||||
setCodeTheme (theme) {
|
||||
GetCodeThemeLink (theme) {
|
||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
||||
? theme
|
||||
: 'elegant'
|
||||
this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized')
|
||||
return theme.startsWith('solarized')
|
||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
||||
}
|
||||
@@ -315,11 +387,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
||||
})
|
||||
}
|
||||
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => {
|
||||
el.parentNode.parentNode.style.listStyleType = 'none'
|
||||
})
|
||||
this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
this.fixDecodedURI(el)
|
||||
@@ -335,9 +403,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
|
||||
el.src = markdown.normalizeLinkText(el.src)
|
||||
el.src = this.markdown.normalizeLinkText(el.src)
|
||||
if (!/\/:storage/.test(el.src)) return
|
||||
el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
||||
el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
||||
})
|
||||
|
||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||
@@ -364,9 +432,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.innerHTML = ''
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror`
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||
} else {
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||
}
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: indentSize
|
||||
@@ -449,9 +517,20 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
handlelinkClick (e) {
|
||||
const noteHash = e.target.href.split('/').pop()
|
||||
const regexIsNoteLink = /^(.{20})-(.{20})$/
|
||||
// this will match the new uuid v4 hash and the old hash
|
||||
// e.g.
|
||||
// :note:1c211eb7dcb463de6490 and
|
||||
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
|
||||
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
|
||||
if (regexIsNoteLink.test(noteHash)) {
|
||||
eventEmitter.emit('list:jump', noteHash)
|
||||
eventEmitter.emit('list:jump', noteHash.replace(':note:', ''))
|
||||
}
|
||||
// this will match the old link format storage.key-note.key
|
||||
// e.g.
|
||||
// 877f99c3268608328037-1c211eb7dcb463de6490
|
||||
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
|
||||
if (regexIsLegacyNoteLink.test(noteHash)) {
|
||||
eventEmitter.emit('list:jump', noteHash.split('-')[1])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,5 +557,6 @@ MarkdownPreview.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
showCopyNotification: PropTypes.bool,
|
||||
storagePath: PropTypes.string
|
||||
storagePath: PropTypes.string,
|
||||
smartQuotes: PropTypes.bool
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import _ from 'lodash'
|
||||
|
||||
import styles from './MarkdownSplitEditor.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
@@ -12,6 +13,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
this.value = props.value
|
||||
this.focus = () => this.refs.code.focus()
|
||||
this.reload = () => this.refs.code.reload()
|
||||
this.userScroll = true
|
||||
}
|
||||
|
||||
handleOnChange () {
|
||||
@@ -19,6 +21,49 @@ class MarkdownSplitEditor extends React.Component {
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||
let srcTop, srcHeight, targetTop, targetHeight
|
||||
|
||||
if (this.userScroll) {
|
||||
if (e.doc) {
|
||||
srcTop = _.get(e, 'doc.scrollTop')
|
||||
srcHeight = _.get(e, 'doc.height')
|
||||
targetTop = _.get(previewDoc, 'body.scrollTop')
|
||||
targetHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
} else {
|
||||
srcTop = _.get(previewDoc, 'body.scrollTop')
|
||||
srcHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
targetTop = _.get(codeDoc, 'scrollTop')
|
||||
targetHeight = _.get(codeDoc, 'height')
|
||||
}
|
||||
|
||||
const distance = (targetHeight * srcTop / srcHeight) - targetTop
|
||||
const framerate = 1000 / 60
|
||||
const frames = 20
|
||||
const refractory = frames * framerate
|
||||
|
||||
this.userScroll = false
|
||||
|
||||
let frame = 0
|
||||
let scrollPos, time
|
||||
const timer = setInterval(() => {
|
||||
time = frame / frames
|
||||
scrollPos = time < 0.5
|
||||
? 2 * time * time // ease in
|
||||
: -1 + (4 - 2 * time) * time // ease out
|
||||
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
|
||||
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance)
|
||||
if (frame >= frames) {
|
||||
clearInterval(timer)
|
||||
setTimeout(() => { this.userScroll = true }, refractory)
|
||||
}
|
||||
frame++
|
||||
}, framerate)
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckboxClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@@ -66,8 +111,10 @@ class MarkdownSplitEditor extends React.Component {
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
storageKey={storageKey}
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
/>
|
||||
<MarkdownPreview
|
||||
style={previewStyle}
|
||||
@@ -80,10 +127,12 @@ class MarkdownSplitEditor extends React.Component {
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
ref='preview'
|
||||
tabInde='0'
|
||||
value={value}
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
/>
|
||||
|
||||
@@ -46,14 +46,25 @@ const TagElementList = (tags) => {
|
||||
* @param {Function} handleDragStart
|
||||
* @param {string} dateDisplay
|
||||
*/
|
||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
||||
const NoteItem = ({
|
||||
isActive,
|
||||
note,
|
||||
dateDisplay,
|
||||
handleNoteClick,
|
||||
handleNoteContextMenu,
|
||||
handleDragStart,
|
||||
pathname,
|
||||
storageName,
|
||||
folderName,
|
||||
viewType
|
||||
}) => (
|
||||
<div styleName={isActive
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
key={`${note.storage}-${note.key}`}
|
||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
key={note.key}
|
||||
onClick={e => handleNoteClick(e, note.key)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
@@ -68,23 +79,33 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteCont
|
||||
: <span styleName='item-title-empty'>Empty</span>
|
||||
}
|
||||
</div>
|
||||
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'>
|
||||
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||
<div styleName='item-middle-app-meta'>
|
||||
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'>
|
||||
{viewType === 'ALL' && storageName}
|
||||
{viewType === 'STORAGE' && folderName}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div styleName='item-bottom-time'>{dateDisplay}</div>
|
||||
{note.isStarred
|
||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||
}
|
||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||
}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''
|
||||
}
|
||||
<div styleName='item-bottom'>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{note.tags.length > 0
|
||||
? TagElementList(note.tags)
|
||||
: <span styleName='item-bottom-tagList-empty' />
|
||||
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>No tags</span>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
{note.isStarred
|
||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||
}
|
||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||
}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,7 +123,11 @@ NoteItem.propTypes = {
|
||||
title: PropTypes.string.isrequired,
|
||||
tags: PropTypes.array,
|
||||
isStarred: PropTypes.bool.isRequired,
|
||||
isTrashed: PropTypes.bool.isRequired
|
||||
isTrashed: PropTypes.bool.isRequired,
|
||||
blog: {
|
||||
blogLink: PropTypes.string,
|
||||
blogId: PropTypes.number
|
||||
}
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||
|
||||
@@ -90,6 +90,26 @@ $control-height = 30px
|
||||
font-weight normal
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-middle
|
||||
font-size 13px
|
||||
padding-left 2px
|
||||
padding-bottom 2px
|
||||
|
||||
.item-middle-time
|
||||
color $ui-inactive-text-color
|
||||
display inline-block
|
||||
|
||||
.item-middle-app-meta
|
||||
float right
|
||||
.item-middle-app-meta-label
|
||||
opacity 0.4
|
||||
color $ui-inactive-text-color
|
||||
padding 0 3px
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
max-width 200px
|
||||
|
||||
.item-bottom
|
||||
position relative
|
||||
bottom 0px
|
||||
@@ -97,7 +117,7 @@ $control-height = 30px
|
||||
font-size 12px
|
||||
line-height 20px
|
||||
overflow ellipsis
|
||||
display flex
|
||||
display block
|
||||
|
||||
.item-bottom-tagList
|
||||
flex 1
|
||||
@@ -125,10 +145,8 @@ $control-height = 30px
|
||||
|
||||
.item-star
|
||||
position absolute
|
||||
right -6px
|
||||
bottom 23px
|
||||
width 16px
|
||||
height 16px
|
||||
right 2px
|
||||
top 5px
|
||||
color alpha($ui-favorite-star-button-color, 60%)
|
||||
font-size 12px
|
||||
padding 0
|
||||
@@ -136,10 +154,8 @@ $control-height = 30px
|
||||
|
||||
.item-pin
|
||||
position absolute
|
||||
right 0px
|
||||
bottom 2px
|
||||
width 34px
|
||||
height 34px
|
||||
right 25px
|
||||
top 7px
|
||||
color #E54D42
|
||||
font-size 14px
|
||||
padding 0
|
||||
@@ -192,7 +208,7 @@ body[data-theme="dark"]
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-dark-text-color
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-wrapper
|
||||
border-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
@@ -266,7 +282,7 @@ body[data-theme="solarized-dark"]
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 10%)
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.item-wrapper
|
||||
border-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
|
||||
@@ -304,4 +320,4 @@ body[data-theme="solarized-dark"]
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
vertical-align middle
|
||||
|
||||
@@ -14,14 +14,23 @@ import styles from './NoteItemSimple.styl'
|
||||
* @param {Function} handleNoteContextMenu
|
||||
* @param {Function} handleDragStart
|
||||
*/
|
||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
||||
const NoteItemSimple = ({
|
||||
isActive,
|
||||
isAllNotesView,
|
||||
note,
|
||||
handleNoteClick,
|
||||
handleNoteContextMenu,
|
||||
handleDragStart,
|
||||
pathname,
|
||||
storage
|
||||
}) => (
|
||||
<div styleName={isActive
|
||||
? 'item-simple--active'
|
||||
: 'item-simple'
|
||||
}
|
||||
key={`${note.storage}-${note.key}`}
|
||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||
? 'item-simple--active'
|
||||
: 'item-simple'
|
||||
}
|
||||
key={note.key}
|
||||
onClick={e => handleNoteClick(e, note.key)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
@@ -30,7 +39,7 @@ const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu
|
||||
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
|
||||
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
||||
}
|
||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||
: ''
|
||||
}
|
||||
@@ -38,6 +47,11 @@ const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu
|
||||
? note.title
|
||||
: <span styleName='item-simple-title-empty'>Empty</span>
|
||||
}
|
||||
{isAllNotesView && <div styleName='item-simple-right'>
|
||||
<span styleName='item-simple-right-storageName'>
|
||||
{storage.name}
|
||||
</span>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -124,7 +124,7 @@ body[data-theme="dark"]
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-dark-text-color
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-dark-borderColor
|
||||
@@ -188,7 +188,7 @@ body[data-theme="solarized-dark"]
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
@@ -206,4 +206,9 @@ body[data-theme="solarized-dark"]
|
||||
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
background-color alpha(#fff, 20%)
|
||||
.item-simple-right
|
||||
float right
|
||||
.item-simple-right-storageName
|
||||
padding-left 4px
|
||||
opacity 0.4
|
||||
|
||||
@@ -17,7 +17,7 @@ import styles from './SideNavFilter.styl'
|
||||
const SideNavFilter = ({
|
||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
||||
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
||||
counterTotalNote, counterStarredNote
|
||||
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
|
||||
}) => (
|
||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||
|
||||
@@ -26,9 +26,9 @@ const SideNavFilter = ({
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isHomeActive
|
||||
? '../resources/icon/icon-all-active.svg'
|
||||
: '../resources/icon/icon-all.svg'
|
||||
}
|
||||
? '../resources/icon/icon-all-active.svg'
|
||||
: '../resources/icon/icon-all.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>All Notes</span>
|
||||
@@ -40,9 +40,9 @@ const SideNavFilter = ({
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isStarredActive
|
||||
? '../resources/icon/icon-star-active.svg'
|
||||
: '../resources/icon/icon-star-sidenav.svg'
|
||||
}
|
||||
? '../resources/icon/icon-star-active.svg'
|
||||
: '../resources/icon/icon-star-sidenav.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>Starred</span>
|
||||
@@ -54,12 +54,12 @@ const SideNavFilter = ({
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isTrashedActive
|
||||
? '../resources/icon/icon-trash-active.svg'
|
||||
: '../resources/icon/icon-trash-sidenav.svg'
|
||||
}
|
||||
? '../resources/icon/icon-trash-active.svg'
|
||||
: '../resources/icon/icon-trash-sidenav.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>Trash</span>
|
||||
<span onContextMenu={handleFilterButtonContextMenu} styleName='menu-button-label'>Trash</span>
|
||||
<span styleName='counters'>{counterDelNote}</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -6,6 +6,18 @@ import React from 'react'
|
||||
import styles from './StorageItem.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import _ from 'lodash'
|
||||
import { SortableHandle } from 'react-sortable-hoc'
|
||||
|
||||
const DraggableIcon = SortableHandle(({ className }) => (
|
||||
<i className={`fa ${className}`} />
|
||||
))
|
||||
|
||||
const FolderIcon = ({ className, color, isActive }) => {
|
||||
const iconStyle = isActive ? 'fa-folder-open-o' : 'fa-folder-o'
|
||||
return (
|
||||
<i className={`fa ${iconStyle} ${className}`} style={{ color: color }} />
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} isActive
|
||||
@@ -21,34 +33,54 @@ import _ from 'lodash'
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const StorageItem = ({
|
||||
isActive, handleButtonClick, handleContextMenu, folderName,
|
||||
folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave
|
||||
}) => (
|
||||
<button styleName={isActive
|
||||
? 'folderList-item--active'
|
||||
: 'folderList-item'
|
||||
}
|
||||
onClick={handleButtonClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<span styleName={isFolded
|
||||
? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
}>
|
||||
<text style={{color: folderColor, paddingRight: '10px'}}>{isActive ? <i className='fa fa-folder-open-o' /> : <i className='fa fa-folder-o' />}</text>{isFolded ? _.truncate(folderName, {length: 1, omission: ''}) : folderName}
|
||||
</span>
|
||||
{(!isFolded && _.isNumber(noteCount)) &&
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||
}
|
||||
{isFolded &&
|
||||
<span styleName='folderList-item-tooltip'>
|
||||
{folderName}
|
||||
styles,
|
||||
isActive,
|
||||
handleButtonClick,
|
||||
handleContextMenu,
|
||||
folderName,
|
||||
folderColor,
|
||||
isFolded,
|
||||
noteCount,
|
||||
handleDrop,
|
||||
handleDragEnter,
|
||||
handleDragLeave
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
|
||||
onClick={handleButtonClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
{!isFolded && (
|
||||
<DraggableIcon className={styles['folderList-item-reorder']} />
|
||||
)}
|
||||
<span
|
||||
styleName={
|
||||
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
}
|
||||
>
|
||||
<FolderIcon
|
||||
styleName='folderList-item-icon'
|
||||
color={folderColor}
|
||||
isActive={isActive}
|
||||
/>
|
||||
{isFolded
|
||||
? _.truncate(folderName, { length: 1, omission: '' })
|
||||
: folderName}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
)
|
||||
{!isFolded &&
|
||||
_.isNumber(noteCount) && (
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||
)}
|
||||
{isFolded && (
|
||||
<span styleName='folderList-item-tooltip'>{folderName}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
StorageItem.propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
border none
|
||||
overflow ellipsis
|
||||
font-size 14px
|
||||
align-items: center
|
||||
&:first-child
|
||||
margin-top 0
|
||||
&:hover
|
||||
@@ -22,7 +23,7 @@
|
||||
&:active
|
||||
color $$ui-button-default-color
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color #1EC38B
|
||||
@@ -34,9 +35,7 @@
|
||||
.folderList-item-name
|
||||
display block
|
||||
flex 1
|
||||
padding 0 12px
|
||||
height 26px
|
||||
line-height 26px
|
||||
padding-right: 10px
|
||||
border-width 0 0 0 2px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
@@ -69,9 +68,20 @@
|
||||
.folderList-item-name--folded
|
||||
@extend .folderList-item-name
|
||||
padding-left 7px
|
||||
text
|
||||
.folderList-item-icon
|
||||
font-size 9px
|
||||
|
||||
.folderList-item-icon
|
||||
padding-right: 10px
|
||||
|
||||
.folderList-item-reorder
|
||||
font-size: 9px
|
||||
padding: 10px 8px 10px 9px;
|
||||
color: rgba(147, 147, 149, 0.3)
|
||||
cursor: ns-resize
|
||||
&:before
|
||||
content: "\f142 \f142"
|
||||
|
||||
body[data-theme="white"]
|
||||
.folderList-item
|
||||
color $ui-inactive-text-color
|
||||
@@ -127,4 +137,4 @@ body[data-theme="solarized-dark"]
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
|
||||
@@ -12,10 +12,11 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
* @param {bool} isActive
|
||||
*/
|
||||
|
||||
const TagListItem = ({name, handleClickTagListItem, isActive}) => (
|
||||
const TagListItem = ({name, handleClickTagListItem, isActive, count}) => (
|
||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||
<span styleName='tagList-item-name'>
|
||||
{`# ${name}`}
|
||||
<span styleName='tagList-item-count'> {count}</span>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
.tagList-item-count
|
||||
padding 0 3px
|
||||
|
||||
body[data-theme="white"]
|
||||
.tagList-item
|
||||
color $ui-inactive-text-color
|
||||
@@ -63,6 +66,8 @@ body[data-theme="white"]
|
||||
color $ui-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
.tagList-item-count
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.tagList-item
|
||||
@@ -81,4 +86,6 @@ body[data-theme="dark"]
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
.tagList-item-count
|
||||
color $ui-dark-button--active-color
|
||||
|
||||
@@ -76,7 +76,7 @@ body
|
||||
justify-content left
|
||||
li
|
||||
label.taskListItem
|
||||
margin-left -2em
|
||||
margin-left -1.8em
|
||||
&.checked
|
||||
text-decoration line-through
|
||||
opacity 0.5
|
||||
@@ -178,6 +178,8 @@ ul
|
||||
margin-bottom 1em
|
||||
li
|
||||
display list-item
|
||||
&.taskListItem
|
||||
list-style none
|
||||
p
|
||||
margin 0
|
||||
&>li>ul, &>li>ol
|
||||
@@ -218,6 +220,7 @@ pre
|
||||
background-color white
|
||||
&.CodeMirror
|
||||
height initial
|
||||
flex-wrap wrap
|
||||
&>code
|
||||
flex 1
|
||||
overflow-x auto
|
||||
@@ -227,6 +230,13 @@ pre
|
||||
padding 0
|
||||
border none
|
||||
border-radius 0
|
||||
&>span.filename
|
||||
width 100%
|
||||
border-radius: 5px 0px 0px 0px
|
||||
margin -8px 100% 8px -8px
|
||||
padding 0px 6px
|
||||
background-color #777;
|
||||
color white
|
||||
&>span.lineNumber
|
||||
display none
|
||||
font-size 1em
|
||||
|
||||
0
browser/finder/NoteDetail.js
Normal file
0
browser/finder/NoteDetail.js
Normal file
@@ -1,7 +1,11 @@
|
||||
const crypto = require('crypto')
|
||||
const _ = require('lodash')
|
||||
const uuidv4 = require('uuid/v4')
|
||||
|
||||
module.exports = function (length) {
|
||||
if (!_.isFinite(length)) length = 10
|
||||
module.exports = function (uuid) {
|
||||
if (typeof uuid === typeof true && uuid) {
|
||||
return uuidv4()
|
||||
}
|
||||
const length = 10
|
||||
return crypto.randomBytes(length).toString('hex')
|
||||
}
|
||||
|
||||
23
browser/lib/markdown-it-sanitize-html.js
Normal file
23
browser/lib/markdown-it-sanitize-html.js
Normal file
@@ -0,0 +1,23 @@
|
||||
'use strict'
|
||||
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
|
||||
module.exports = function sanitizePlugin (md, options) {
|
||||
options = options || {}
|
||||
|
||||
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
||||
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
||||
if (state.tokens[tokenIdx].type === 'html_block') {
|
||||
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options)
|
||||
}
|
||||
if (state.tokens[tokenIdx].type === 'inline') {
|
||||
const inlineTokens = state.tokens[tokenIdx].children
|
||||
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
||||
if (inlineTokens[childIdx].type === 'html_inline') {
|
||||
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,173 +1,235 @@
|
||||
import markdownit from 'markdown-it'
|
||||
import sanitize from './markdown-it-sanitize-html'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import math from '@rokt33r/markdown-it-math'
|
||||
import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import {lastFindInArray} from './utils'
|
||||
|
||||
// FIXME We should not depend on global variable.
|
||||
const katex = window.katex
|
||||
const config = ConfigManager.get()
|
||||
|
||||
function createGutter (str) {
|
||||
const lc = (str.match(/\n/g) || []).length
|
||||
function createGutter (str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
|
||||
const lines = []
|
||||
for (let i = 1; i <= lc; i++) {
|
||||
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
|
||||
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
||||
}
|
||||
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
||||
}
|
||||
|
||||
var md = markdownit({
|
||||
typographer: true,
|
||||
linkify: true,
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: true,
|
||||
highlight: function (str, lang) {
|
||||
if (lang === 'flowchart') {
|
||||
return `<pre class="flowchart">${str}</pre>`
|
||||
}
|
||||
if (lang === 'sequence') {
|
||||
return `<pre class="sequence">${str}</pre>`
|
||||
}
|
||||
return '<pre class="code">' +
|
||||
createGutter(str) +
|
||||
'<code class="' + lang + '">' +
|
||||
str +
|
||||
'</code></pre>'
|
||||
}
|
||||
})
|
||||
md.use(emoji, {
|
||||
shortcuts: {}
|
||||
})
|
||||
md.use(math, {
|
||||
inlineOpen: config.preview.latexInlineOpen,
|
||||
inlineClose: config.preview.latexInlineClose,
|
||||
blockOpen: config.preview.latexBlockOpen,
|
||||
blockClose: config.preview.latexBlockClose,
|
||||
inlineRenderer: function (str) {
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim())
|
||||
} catch (err) {
|
||||
output = `<span class="katex-error">${err.message}</span>`
|
||||
}
|
||||
return output
|
||||
},
|
||||
blockRenderer: function (str) {
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim(), { displayMode: true })
|
||||
} catch (err) {
|
||||
output = `<div class="katex-error">${err.message}</div>`
|
||||
}
|
||||
return output
|
||||
}
|
||||
})
|
||||
md.use(require('markdown-it-imsize'))
|
||||
md.use(require('markdown-it-footnote'))
|
||||
md.use(require('markdown-it-multimd-table'))
|
||||
md.use(require('markdown-it-named-headers'), {
|
||||
slugify: (header) => {
|
||||
return encodeURI(header.trim()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||
.replace(/\s+/g, '-'))
|
||||
.replace(/\-+$/, '')
|
||||
}
|
||||
})
|
||||
md.use(require('markdown-it-kbd'))
|
||||
class Markdown {
|
||||
constructor (options = {}) {
|
||||
const defaultOptions = {
|
||||
typographer: true,
|
||||
linkify: true,
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: true,
|
||||
highlight: function (str, lang) {
|
||||
const delimiter = ':'
|
||||
const langInfo = lang.split(delimiter)
|
||||
const langType = langInfo[0]
|
||||
const fileName = langInfo[1] || ''
|
||||
const firstLineNumber = parseInt(langInfo[2], 10)
|
||||
|
||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||
md.use(require('markdown-it-plantuml'), '', {
|
||||
generateSource: function (umlCode) {
|
||||
const s = unescape(encodeURIComponent(umlCode))
|
||||
const zippedCode = deflate.encode64(
|
||||
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
||||
)
|
||||
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
|
||||
}
|
||||
})
|
||||
|
||||
// Override task item
|
||||
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
let content, terminate, i, l, token
|
||||
let nextLine = startLine + 1
|
||||
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||
const endLine = state.lineMax
|
||||
|
||||
// jump line-by-line until empty one or EOF
|
||||
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
||||
// this would be a code block normally, but after paragraph
|
||||
// it's considered a lazy continuation regardless of what's there
|
||||
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
|
||||
|
||||
// quirk for blockquotes, this line should already be checked by that rule
|
||||
if (state.sCount[nextLine] < 0) { continue }
|
||||
|
||||
// Some tags can terminate paragraph without empty line.
|
||||
terminate = false
|
||||
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
||||
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
||||
terminate = true
|
||||
break
|
||||
if (langType === 'flowchart') {
|
||||
return `<pre class="flowchart">${str}</pre>`
|
||||
}
|
||||
if (langType === 'sequence') {
|
||||
return `<pre class="sequence">${str}</pre>`
|
||||
}
|
||||
return '<pre class="code CodeMirror">' +
|
||||
'<span class="filename">' + fileName + '</span>' +
|
||||
createGutter(str, firstLineNumber) +
|
||||
'<code class="' + langType + '">' +
|
||||
str +
|
||||
'</code></pre>'
|
||||
}
|
||||
}
|
||||
if (terminate) { break }
|
||||
|
||||
const updatedOptions = Object.assign(defaultOptions, options)
|
||||
this.md = markdownit(updatedOptions)
|
||||
|
||||
// Sanitize use rinput before other plugins
|
||||
this.md.use(sanitize, {
|
||||
allowedTags: ['iframe', 'input',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
|
||||
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote',
|
||||
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
|
||||
],
|
||||
allowedAttributes: {
|
||||
'*': [
|
||||
'abbr', 'accept', 'accept-charset',
|
||||
'accesskey', 'action', 'align', 'alt', 'axis',
|
||||
'border', 'cellpadding', 'cellspacing', 'char',
|
||||
'charoff', 'charset', 'checked',
|
||||
'clear', 'cols', 'colspan', 'color',
|
||||
'compact', 'coords', 'datetime', 'dir',
|
||||
'disabled', 'enctype', 'for', 'frame',
|
||||
'headers', 'height', 'hreflang',
|
||||
'hspace', 'ismap', 'label', 'lang',
|
||||
'maxlength', 'media', 'method',
|
||||
'multiple', 'name', 'nohref', 'noshade',
|
||||
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
|
||||
'rows', 'rowspan', 'rules', 'scope',
|
||||
'selected', 'shape', 'size', 'span',
|
||||
'start', 'summary', 'tabindex', 'target',
|
||||
'title', 'type', 'usemap', 'valign', 'value',
|
||||
'vspace', 'width', 'itemprop'
|
||||
],
|
||||
'a': ['href'],
|
||||
'div': ['itemscope', 'itemtype'],
|
||||
'blockquote': ['cite'],
|
||||
'del': ['cite'],
|
||||
'ins': ['cite'],
|
||||
'q': ['cite'],
|
||||
'img': ['src', 'width', 'height'],
|
||||
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
||||
'input': ['type', 'id', 'checked']
|
||||
},
|
||||
allowedIframeHostnames: ['www.youtube.com']
|
||||
})
|
||||
|
||||
this.md.use(emoji, {
|
||||
shortcuts: {}
|
||||
})
|
||||
this.md.use(math, {
|
||||
inlineOpen: config.preview.latexInlineOpen,
|
||||
inlineClose: config.preview.latexInlineClose,
|
||||
blockOpen: config.preview.latexBlockOpen,
|
||||
blockClose: config.preview.latexBlockClose,
|
||||
inlineRenderer: function (str) {
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim())
|
||||
} catch (err) {
|
||||
output = `<span class="katex-error">${err.message}</span>`
|
||||
}
|
||||
return output
|
||||
},
|
||||
blockRenderer: function (str) {
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim(), { displayMode: true })
|
||||
} catch (err) {
|
||||
output = `<div class="katex-error">${err.message}</div>`
|
||||
}
|
||||
return output
|
||||
}
|
||||
})
|
||||
this.md.use(require('markdown-it-imsize'))
|
||||
this.md.use(require('markdown-it-footnote'))
|
||||
this.md.use(require('markdown-it-multimd-table'))
|
||||
this.md.use(require('markdown-it-named-headers'), {
|
||||
slugify: (header) => {
|
||||
return encodeURI(header.trim()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||
.replace(/\s+/g, '-'))
|
||||
.replace(/\-+$/, '')
|
||||
}
|
||||
})
|
||||
this.md.use(require('markdown-it-kbd'))
|
||||
|
||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||
this.md.use(require('markdown-it-plantuml'), '', {
|
||||
generateSource: function (umlCode) {
|
||||
const s = unescape(encodeURIComponent(umlCode))
|
||||
const zippedCode = deflate.encode64(
|
||||
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
||||
)
|
||||
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
|
||||
}
|
||||
})
|
||||
|
||||
// Override task item
|
||||
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
let content, terminate, i, l, token
|
||||
let nextLine = startLine + 1
|
||||
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||
const endLine = state.lineMax
|
||||
|
||||
// jump line-by-line until empty one or EOF
|
||||
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
||||
// this would be a code block normally, but after paragraph
|
||||
// it's considered a lazy continuation regardless of what's there
|
||||
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
|
||||
|
||||
// quirk for blockquotes, this line should already be checked by that rule
|
||||
if (state.sCount[nextLine] < 0) { continue }
|
||||
|
||||
// Some tags can terminate paragraph without empty line.
|
||||
terminate = false
|
||||
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
||||
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
||||
terminate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (terminate) { break }
|
||||
}
|
||||
|
||||
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
||||
|
||||
state.line = nextLine
|
||||
|
||||
token = state.push('paragraph_open', 'p', 1)
|
||||
token.map = [startLine, state.line]
|
||||
|
||||
if (state.parentType === 'list') {
|
||||
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||
if (match) {
|
||||
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
|
||||
if (liToken) {
|
||||
if (!liToken.attrs) {
|
||||
liToken.attrs = []
|
||||
}
|
||||
liToken.attrs.push(['class', 'taskListItem'])
|
||||
}
|
||||
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||
}
|
||||
}
|
||||
|
||||
token = state.push('inline', '', 0)
|
||||
token.content = content
|
||||
token.map = [startLine, state.line]
|
||||
token.children = []
|
||||
|
||||
token = state.push('paragraph_close', 'p', -1)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Add line number attribute for scrolling
|
||||
const originalRender = this.md.renderer.render
|
||||
this.md.renderer.render = (tokens, options, env) => {
|
||||
tokens.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case 'heading_open':
|
||||
case 'paragraph_open':
|
||||
case 'blockquote_open':
|
||||
case 'table_open':
|
||||
token.attrPush(['data-line', token.map[0]])
|
||||
}
|
||||
})
|
||||
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
||||
return result
|
||||
}
|
||||
// FIXME We should not depend on global variable.
|
||||
window.md = this.md
|
||||
}
|
||||
|
||||
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
||||
|
||||
state.line = nextLine
|
||||
|
||||
token = state.push('paragraph_open', 'p', 1)
|
||||
token.map = [startLine, state.line]
|
||||
|
||||
if (state.parentType === 'list') {
|
||||
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||
if (match) {
|
||||
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||
}
|
||||
}
|
||||
|
||||
token = state.push('inline', '', 0)
|
||||
token.content = content
|
||||
token.map = [startLine, state.line]
|
||||
token.children = []
|
||||
|
||||
token = state.push('paragraph_close', 'p', -1)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Add line number attribute for scrolling
|
||||
const originalRender = md.renderer.render
|
||||
md.renderer.render = function render (tokens, options, env) {
|
||||
tokens.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case 'heading_open':
|
||||
case 'paragraph_open':
|
||||
case 'blockquote_open':
|
||||
case 'table_open':
|
||||
token.attrPush(['data-line', token.map[0]])
|
||||
}
|
||||
})
|
||||
const result = originalRender.call(md.renderer, tokens, options, env)
|
||||
return result
|
||||
}
|
||||
// FIXME We should not depend on global variable.
|
||||
window.md = md
|
||||
|
||||
function normalizeLinkText (linkText) {
|
||||
return md.normalizeLinkText(linkText)
|
||||
}
|
||||
|
||||
const markdown = {
|
||||
render: function markdown (content) {
|
||||
render (content) {
|
||||
if (!_.isString(content)) content = ''
|
||||
const renderedContent = md.render(content)
|
||||
return renderedContent
|
||||
},
|
||||
normalizeLinkText
|
||||
return this.md.render(content)
|
||||
}
|
||||
|
||||
normalizeLinkText (linkText) {
|
||||
return this.md.normalizeLinkText(linkText)
|
||||
}
|
||||
}
|
||||
|
||||
export default markdown
|
||||
export default Markdown
|
||||
|
||||
|
||||
11
browser/lib/utils.js
Normal file
11
browser/lib/utils.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export function lastFindInArray (array, callback) {
|
||||
for (let i = array.length - 1; i >= 0; --i) {
|
||||
if (callback(array[i], i, array)) {
|
||||
return array[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
lastFindInArray
|
||||
}
|
||||
22
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
22
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
@@ -19,6 +19,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import TrashButton from './TrashButton'
|
||||
import FullscreenButton from './FullscreenButton'
|
||||
import RestoreButton from './RestoreButton'
|
||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import ToggleModeButton from './ToggleModeButton'
|
||||
@@ -68,11 +69,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
componentDidUnmount () {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
handleUpdateTag () {
|
||||
@@ -141,7 +139,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
key: newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
@@ -198,8 +196,9 @@ class MarkdownNoteDetail extends React.Component {
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.once('list:next', dispatchHandler)
|
||||
})
|
||||
.then(() => ee.emit('list:next'))
|
||||
}
|
||||
} else {
|
||||
if (confirmDeletion()) {
|
||||
@@ -323,10 +322,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
@@ -361,12 +357,10 @@ class MarkdownNoteDetail extends React.Component {
|
||||
value={this.state.note.tags}
|
||||
onChange={this.handleUpdateTag.bind(this)}
|
||||
/>
|
||||
|
||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||
|
||||
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||
<StarButton
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
@@ -399,7 +393,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.exportAsMd}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
padding 20px 40px
|
||||
overflow hidden
|
||||
|
||||
.lock-button
|
||||
padding-bottom 3px
|
||||
@@ -44,7 +45,7 @@
|
||||
margin 0 30px
|
||||
.body-noteEditor
|
||||
absolute top bottom left right
|
||||
|
||||
|
||||
body[data-theme="white"]
|
||||
.root
|
||||
box-shadow $note-detail-box-shadow
|
||||
|
||||
21
browser/main/Detail/RestoreButton.js
Normal file
21
browser/main/Detail/RestoreButton.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './RestoreButton.styl'
|
||||
|
||||
const RestoreButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-restoreButton'
|
||||
onClick={onClick}
|
||||
>
|
||||
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
|
||||
<span styleName='tooltip'>Restore</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
RestoreButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(RestoreButton, styles)
|
||||
22
browser/main/Detail/RestoreButton.styl
Normal file
22
browser/main/Detail/RestoreButton.styl
Normal file
@@ -0,0 +1,22 @@
|
||||
.control-restoreButton
|
||||
top 115px
|
||||
topBarButtonRight()
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 50px
|
||||
left 25px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-restoreButton
|
||||
topBarButtonDark()
|
||||
@@ -20,6 +20,7 @@ import _ from 'lodash'
|
||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import TrashButton from './TrashButton'
|
||||
import RestoreButton from './RestoreButton'
|
||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import InfoPanel from './InfoPanel'
|
||||
@@ -146,7 +147,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
key: newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
@@ -191,8 +192,9 @@ class SnippetNoteDetail extends React.Component {
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.once('list:next', dispatchHandler)
|
||||
})
|
||||
.then(() => ee.emit('list:next'))
|
||||
}
|
||||
} else {
|
||||
if (confirmDeletion()) {
|
||||
@@ -341,6 +343,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// tab key
|
||||
case 9:
|
||||
if (e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
@@ -353,6 +356,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.focusEditor()
|
||||
}
|
||||
break
|
||||
// L key
|
||||
case 76:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
@@ -364,6 +368,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
}
|
||||
break
|
||||
// T key
|
||||
case 84:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
@@ -567,6 +572,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
keyMap={config.editor.keyMap}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
@@ -587,10 +593,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
@@ -647,7 +650,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
width 52px
|
||||
display flex
|
||||
align-items center
|
||||
position absolute
|
||||
right 165px
|
||||
position: relative
|
||||
top 2px
|
||||
.active
|
||||
background-color #1EC38B
|
||||
width 33px
|
||||
@@ -55,4 +55,4 @@ body[data-theme="solarized-dark"]
|
||||
background-color #002B36
|
||||
.active
|
||||
background-color #1EC38B
|
||||
box-shadow 2px 0px 7px #222222
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
@@ -56,11 +56,8 @@ class Detail extends React.Component {
|
||||
const { location, data, config } = this.props
|
||||
let note = null
|
||||
if (location.query.key != null) {
|
||||
const splitted = location.query.key.split('-')
|
||||
const storageKey = splitted.shift()
|
||||
const noteKey = splitted.shift()
|
||||
|
||||
note = data.noteMap.get(storageKey + '-' + noteKey)
|
||||
const noteKey = location.query.key
|
||||
note = data.noteMap.get(noteKey)
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
|
||||
@@ -13,10 +13,13 @@ import searchFromNotes from 'browser/lib/search'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { hashHistory } from 'react-router'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import markdown from '../../lib/markdown'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
const WP_POST_PATH = '/wp/v2/posts'
|
||||
|
||||
function sortByCreatedAt (a, b) {
|
||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||
@@ -31,7 +34,7 @@ function sortByUpdatedAt (a, b) {
|
||||
}
|
||||
|
||||
function findNoteByKey (notes, noteKey) {
|
||||
return notes.find((note) => `${note.storage}-${note.key}` === noteKey)
|
||||
return notes.find((note) => note.key === noteKey)
|
||||
}
|
||||
|
||||
function findNotesByKeys (notes, noteKeys) {
|
||||
@@ -39,7 +42,7 @@ function findNotesByKeys (notes, noteKeys) {
|
||||
}
|
||||
|
||||
function getNoteKey (note) {
|
||||
return `${note.storage}-${note.key}`
|
||||
return note.key
|
||||
}
|
||||
|
||||
class NoteList extends React.Component {
|
||||
@@ -66,6 +69,11 @@ class NoteList extends React.Component {
|
||||
this.deleteNote = this.deleteNote.bind(this)
|
||||
this.focusNote = this.focusNote.bind(this)
|
||||
this.pinToTop = this.pinToTop.bind(this)
|
||||
this.getNoteStorage = this.getNoteStorage.bind(this)
|
||||
this.getNoteFolder = this.getNoteFolder.bind(this)
|
||||
this.getViewType = this.getViewType.bind(this)
|
||||
this.restoreNote = this.restoreNote.bind(this)
|
||||
this.copyNoteLink = this.copyNoteLink.bind(this)
|
||||
|
||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||
this.state = {
|
||||
@@ -109,14 +117,27 @@ class NoteList extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { location } = this.props
|
||||
const { selectedNoteKeys } = this.state
|
||||
const visibleNoteKeys = this.notes.map(note => note.key)
|
||||
const note = this.notes[0]
|
||||
const prevKey = prevProps.location.query.key
|
||||
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
||||
|
||||
if (this.notes.length > 0 && location.query.key == null) {
|
||||
if (note && location.query.key == null) {
|
||||
const { router } = this.context
|
||||
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
||||
|
||||
// A visible note is an active note
|
||||
if (!selectedNoteKeys.includes(noteKey)) {
|
||||
if (selectedNoteKeys.length === 1) selectedNoteKeys.pop()
|
||||
selectedNoteKeys.push(noteKey)
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
|
||||
router.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: this.notes[0].storage + '-' + this.notes[0].key
|
||||
key: noteKey
|
||||
}
|
||||
})
|
||||
return
|
||||
@@ -240,27 +261,38 @@ class NoteList extends React.Component {
|
||||
handleNoteListKeyDown (e) {
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
|
||||
// A key
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
ee.emit('top:new-note')
|
||||
}
|
||||
|
||||
// D key
|
||||
if (e.keyCode === 68) {
|
||||
e.preventDefault()
|
||||
this.deleteNote()
|
||||
}
|
||||
|
||||
// E key
|
||||
if (e.keyCode === 69) {
|
||||
e.preventDefault()
|
||||
ee.emit('detail:focus')
|
||||
}
|
||||
|
||||
if (e.keyCode === 38) {
|
||||
// F or S key
|
||||
if (e.keyCode === 70 || e.keyCode === 83) {
|
||||
e.preventDefault()
|
||||
ee.emit('top:focus-search')
|
||||
}
|
||||
|
||||
// UP or K key
|
||||
if (e.keyCode === 38 || e.keyCode === 75) {
|
||||
e.preventDefault()
|
||||
this.selectPriorNote()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
// DOWN or J key
|
||||
if (e.keyCode === 40 || e.keyCode === 74) {
|
||||
e.preventDefault()
|
||||
this.selectNextNote()
|
||||
}
|
||||
@@ -440,14 +472,27 @@ class NoteList extends React.Component {
|
||||
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top'
|
||||
const deleteLabel = 'Delete Note'
|
||||
const cloneNote = 'Clone Note'
|
||||
const restoreNote = 'Restore Note'
|
||||
const copyNoteLink = 'Copy Note Link'
|
||||
const publishLabel = 'Publish Blog'
|
||||
const updateLabel = 'Update Blog'
|
||||
const openBlogLabel = 'Open Blog'
|
||||
|
||||
const menu = new Menu()
|
||||
if (!location.pathname.match(/\/home|\/starred|\/trash/)) {
|
||||
if (!location.pathname.match(/\/starred|\/trash/)) {
|
||||
menu.append(new MenuItem({
|
||||
label: pinLabel,
|
||||
click: this.pinToTop
|
||||
}))
|
||||
}
|
||||
|
||||
if (location.pathname.match(/\/trash/)) {
|
||||
menu.append(new MenuItem({
|
||||
label: restoreNote,
|
||||
click: this.restoreNote
|
||||
}))
|
||||
}
|
||||
|
||||
menu.append(new MenuItem({
|
||||
label: deleteLabel,
|
||||
click: this.deleteNote
|
||||
@@ -456,31 +501,75 @@ class NoteList extends React.Component {
|
||||
label: cloneNote,
|
||||
click: this.cloneNote.bind(this)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: copyNoteLink,
|
||||
click: this.copyNoteLink(note)
|
||||
}))
|
||||
if (note.type === 'MARKDOWN_NOTE') {
|
||||
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||
menu.append(new MenuItem({
|
||||
label: updateLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: openBlogLabel,
|
||||
click: () => this.openBlog.bind(this)(note)
|
||||
}))
|
||||
} else {
|
||||
menu.append(new MenuItem({
|
||||
label: publishLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
pinToTop () {
|
||||
updateSelectedNotes (updateFunc, cleanSelection = true) {
|
||||
const { selectedNoteKeys } = this.state
|
||||
const { dispatch } = this.props
|
||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||
|
||||
if (!_.isFunction(updateFunc)) {
|
||||
console.warn('Update function is not defined. No update will happen')
|
||||
updateFunc = (note) => { return note }
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
selectedNotes.map((note) => {
|
||||
note.isPinned = !note.isPinned
|
||||
return dataApi
|
||||
.updateNote(note.storage, note.key, note)
|
||||
})
|
||||
)
|
||||
.then((updatedNotes) => {
|
||||
updatedNotes.forEach((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note
|
||||
selectedNotes.map((note) => {
|
||||
note = updateFunc(note)
|
||||
return dataApi
|
||||
.updateNote(note.storage, note.key, note)
|
||||
})
|
||||
})
|
||||
)
|
||||
.then((updatedNotes) => {
|
||||
updatedNotes.forEach((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (cleanSelection) {
|
||||
this.selectNextNote()
|
||||
}
|
||||
}
|
||||
|
||||
pinToTop () {
|
||||
this.updateSelectedNotes((note) => {
|
||||
note.isPinned = !note.isPinned
|
||||
return note
|
||||
})
|
||||
}
|
||||
|
||||
restoreNote () {
|
||||
this.updateSelectedNotes((note) => {
|
||||
note.isTrashed = false
|
||||
return note
|
||||
})
|
||||
this.setState({ selectedNoteKeys: [] })
|
||||
}
|
||||
|
||||
deleteNote () {
|
||||
@@ -500,11 +589,9 @@ class NoteList extends React.Component {
|
||||
})
|
||||
if (dialogueButtonIndex === 1) return
|
||||
Promise.all(
|
||||
selectedNoteKeys.map((uniqueKey) => {
|
||||
const storageKey = uniqueKey.split('-')[0]
|
||||
const noteKey = uniqueKey.split('-')[1]
|
||||
selectedNotes.map((note) => {
|
||||
return dataApi
|
||||
.deleteNote(storageKey, noteKey)
|
||||
.deleteNote(note.storage, note.key)
|
||||
})
|
||||
)
|
||||
.then((data) => {
|
||||
@@ -565,23 +652,133 @@ class NoteList extends React.Component {
|
||||
content: firstNote.content
|
||||
})
|
||||
.then((note) => {
|
||||
const uniqueKey = note.storage + '-' + note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys: [uniqueKey]
|
||||
selectedNoteKeys: [note.key]
|
||||
})
|
||||
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: uniqueKey}
|
||||
query: {key: note.key}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
copyNoteLink (note) {
|
||||
const noteLink = `[${note.title}](${note.key})`
|
||||
return copy(noteLink)
|
||||
}
|
||||
|
||||
save (note) {
|
||||
const { dispatch } = this.props
|
||||
dataApi
|
||||
.updateNote(note.storage, note.key, note)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
publishMarkdown () {
|
||||
if (this.pendingPublish) {
|
||||
clearTimeout(this.pendingPublish)
|
||||
}
|
||||
this.pendingPublish = setTimeout(() => {
|
||||
this.publishMarkdownNow()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
publishMarkdownNow () {
|
||||
const {selectedNoteKeys} = this.state
|
||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||
const firstNote = selectedNotes[0]
|
||||
const config = ConfigManager.get()
|
||||
const {address, token, authMethod, username, password} = config.blog
|
||||
let authToken = ''
|
||||
if (authMethod === 'USER') {
|
||||
authToken = `Basic ${window.btoa(`${username}:${password}`)}`
|
||||
} else {
|
||||
authToken = `Bearer ${token}`
|
||||
}
|
||||
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
|
||||
var data = {
|
||||
title: firstNote.title,
|
||||
content: markdown.render(contentToRender),
|
||||
status: 'publish'
|
||||
}
|
||||
|
||||
let url = ''
|
||||
let method = ''
|
||||
if (firstNote.blog && firstNote.blog.blogId) {
|
||||
url = `${address}${WP_POST_PATH}/${firstNote.blog.blogId}`
|
||||
method = 'PUT'
|
||||
} else {
|
||||
url = `${address}${WP_POST_PATH}`
|
||||
method = 'POST'
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
fetch(url, {
|
||||
method: method,
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Authorization': authToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(res => res.json())
|
||||
.then(response => {
|
||||
if (_.isNil(response.link) || _.isNil(response.id)) {
|
||||
return Promise.reject()
|
||||
}
|
||||
firstNote.blog = {
|
||||
blogLink: response.link,
|
||||
blogId: response.id
|
||||
}
|
||||
this.save(firstNote)
|
||||
this.confirmPublish(firstNote)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.confirmPublishError()
|
||||
})
|
||||
}
|
||||
|
||||
confirmPublishError () {
|
||||
const { remote } = electron
|
||||
const { dialog } = remote
|
||||
const alertError = {
|
||||
type: 'warning',
|
||||
message: 'Publish Failed',
|
||||
detail: 'Check and update your blog setting and try again.',
|
||||
buttons: ['Confirm']
|
||||
}
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), alertError)
|
||||
}
|
||||
|
||||
confirmPublish (note) {
|
||||
const buttonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Publish Succeeded',
|
||||
detail: `${note.title} is published at ${note.blog.blogLink}`,
|
||||
buttons: ['Confirm', 'Open Blog']
|
||||
})
|
||||
|
||||
if (buttonIndex === 1) {
|
||||
this.openBlog(note)
|
||||
}
|
||||
}
|
||||
|
||||
openBlog (note) {
|
||||
const { shell } = electron
|
||||
shell.openExternal(note.blog.blogLink)
|
||||
}
|
||||
|
||||
importFromFile () {
|
||||
const options = {
|
||||
filters: [
|
||||
@@ -678,6 +875,24 @@ class NoteList extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
getNoteStorage (note) { // note.storage = storage key
|
||||
return this.props.data.storageMap.toJS()[note.storage]
|
||||
}
|
||||
|
||||
getNoteFolder (note) { // note.folder = folder key
|
||||
return _.find(this.getNoteStorage(note).folders, ({ key }) => key === note.folder)
|
||||
}
|
||||
|
||||
getViewType () {
|
||||
const { pathname } = this.props.location
|
||||
const folder = /\/folders\/[a-zA-Z0-9]+/.test(pathname)
|
||||
const storage = /\/storages\/[a-zA-Z0-9]+/.test(pathname) && !folder
|
||||
const allNotes = pathname === '/home'
|
||||
if (allNotes) return 'ALL'
|
||||
if (folder) return 'FOLDER'
|
||||
if (storage) return 'STORAGE'
|
||||
}
|
||||
|
||||
render () {
|
||||
const { location, config } = this.props
|
||||
let { notes } = this.props
|
||||
@@ -687,7 +902,7 @@ class NoteList extends React.Component {
|
||||
: config.sortBy === 'ALPHABETICAL'
|
||||
? sortByAlphabetical
|
||||
: sortByUpdatedAt
|
||||
const sortedNotes = location.pathname.match(/\/home|\/starred|\/trash/)
|
||||
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||
? this.getNotes().sort(sortFunc)
|
||||
: this.sortByPin(this.getNotes().sort(sortFunc))
|
||||
this.notes = notes = sortedNotes.filter((note) => {
|
||||
@@ -714,6 +929,8 @@ class NoteList extends React.Component {
|
||||
}
|
||||
})
|
||||
|
||||
const viewType = this.getViewType()
|
||||
|
||||
const noteList = notes
|
||||
.map(note => {
|
||||
if (note == null) {
|
||||
@@ -739,6 +956,9 @@ class NoteList extends React.Component {
|
||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||
handleDragStart={this.handleDragStart.bind(this)}
|
||||
pathname={location.pathname}
|
||||
folderName={this.getNoteFolder(note).name}
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
viewType={viewType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -752,6 +972,9 @@ class NoteList extends React.Component {
|
||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||
handleDragStart={this.handleDragStart.bind(this)}
|
||||
pathname={location.pathname}
|
||||
folderName={this.getNoteFolder(note).name}
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
viewType={viewType}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -766,16 +989,17 @@ class NoteList extends React.Component {
|
||||
<div styleName='control-sortBy'>
|
||||
<i className='fa fa-angle-down' />
|
||||
<select styleName='control-sortBy-select'
|
||||
title='Select filter mode'
|
||||
value={config.sortBy}
|
||||
onChange={(e) => this.handleSortByChange(e)}
|
||||
>
|
||||
<option value='UPDATED_AT'>Updated</option>
|
||||
<option value='CREATED_AT'>Created</option>
|
||||
<option value='ALPHABETICAL'>Alphabetically</option>
|
||||
<option title='Sort by update time' value='UPDATED_AT'>Updated</option>
|
||||
<option title='Sort by create time' value='CREATED_AT'>Created</option>
|
||||
<option title='Sort alphabetically' value='ALPHABETICAL'>Alphabetically</option>
|
||||
</select>
|
||||
</div>
|
||||
<div styleName='control-button-area'>
|
||||
<button styleName={config.listStyle === 'DEFAULT'
|
||||
<button title='Default View' styleName={config.listStyle === 'DEFAULT'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
@@ -783,7 +1007,7 @@ class NoteList extends React.Component {
|
||||
>
|
||||
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
||||
</button>
|
||||
<button styleName={config.listStyle === 'SMALL'
|
||||
<button title='Compressed View' styleName={config.listStyle === 'SMALL'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import StorageItemChild from 'browser/components/StorageItem'
|
||||
import _ from 'lodash'
|
||||
import { SortableElement } from 'react-sortable-hoc'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, dialog } = remote
|
||||
@@ -191,33 +192,16 @@ class StorageItem extends React.Component {
|
||||
dropNote (storage, folder, dispatch, location, noteData) {
|
||||
noteData = noteData.filter((note) => folder.key !== note.folder)
|
||||
if (noteData.length === 0) return
|
||||
const newNoteData = noteData.map((note) => Object.assign({}, note, {storage: storage, folder: folder.key}))
|
||||
|
||||
Promise.all(
|
||||
newNoteData.map((note) => dataApi.createNote(storage.key, note))
|
||||
noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key))
|
||||
)
|
||||
.then((createdNoteData) => {
|
||||
createdNoteData.forEach((note) => {
|
||||
createdNoteData.forEach((newNote) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`error on create notes: ${err}`)
|
||||
})
|
||||
.then(() => {
|
||||
return Promise.all(
|
||||
noteData.map((note) => dataApi.deleteNote(note.storage, note.key))
|
||||
)
|
||||
})
|
||||
.then((deletedNoteData) => {
|
||||
deletedNoteData.forEach((note) => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: note.storageKey,
|
||||
noteKey: note.noteKey
|
||||
type: 'MOVE_NOTE',
|
||||
originNote: noteData.find((note) => note.content === newNote.content),
|
||||
note: newNote
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -236,7 +220,8 @@ class StorageItem extends React.Component {
|
||||
render () {
|
||||
const { storage, location, isFolded, data, dispatch } = this.props
|
||||
const { folderNoteMap, trashedSet } = data
|
||||
const folderList = storage.folders.map((folder) => {
|
||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||
const folderList = storage.folders.map((folder, index) => {
|
||||
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
|
||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||
|
||||
@@ -250,8 +235,9 @@ class StorageItem extends React.Component {
|
||||
noteCount = noteSet.size - trashedNoteCount
|
||||
}
|
||||
return (
|
||||
<StorageItemChild
|
||||
<SortableStorageItemChild
|
||||
key={folder.key}
|
||||
index={index}
|
||||
isActive={isActive}
|
||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||
@@ -273,9 +259,9 @@ class StorageItem extends React.Component {
|
||||
key={storage.key}
|
||||
>
|
||||
<div styleName={isActive
|
||||
? 'header--active'
|
||||
: 'header'
|
||||
}
|
||||
? 'header--active'
|
||||
: 'header'
|
||||
}
|
||||
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
|
||||
>
|
||||
<button styleName='header-toggleButton'
|
||||
@@ -284,7 +270,7 @@ class StorageItem extends React.Component {
|
||||
<img src={this.state.isOpen
|
||||
? '../resources/icon/icon-down.svg'
|
||||
: '../resources/icon/icon-right.svg'
|
||||
}
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
const { remote } = require('electron')
|
||||
const { Menu } = remote
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import styles from './SideNav.styl'
|
||||
import { openModal } from 'browser/main/lib/modal'
|
||||
import PreferencesModal from '../modals/PreferencesModal'
|
||||
@@ -14,6 +17,7 @@ import EventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import PreferenceButton from './PreferenceButton'
|
||||
import ListButton from './ListButton'
|
||||
import TagButton from './TagButton'
|
||||
import {SortableContainer} from 'react-sortable-hoc'
|
||||
|
||||
class SideNav extends React.Component {
|
||||
// TODO: should not use electron stuff v0.7
|
||||
@@ -65,6 +69,17 @@ class SideNav extends React.Component {
|
||||
router.push('/alltags')
|
||||
}
|
||||
|
||||
onSortEnd (storage) {
|
||||
return ({oldIndex, newIndex}) => {
|
||||
const { dispatch } = this.props
|
||||
dataApi
|
||||
.reorderFolder(storage.key, oldIndex, newIndex)
|
||||
.then((data) => {
|
||||
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
SideNavComponent (isFolded, storageList) {
|
||||
const { location, data } = this.props
|
||||
|
||||
@@ -86,9 +101,10 @@ class SideNav extends React.Component {
|
||||
isTrashedActive={isTrashedActive}
|
||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
||||
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
||||
counterTotalNote={data.noteMap._map.size}
|
||||
counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
|
||||
counterStarredNote={data.starredSet._set.size}
|
||||
counterDelNote={data.trashedSet._set.size}
|
||||
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
|
||||
/>
|
||||
|
||||
<StorageList storageList={storageList} />
|
||||
@@ -113,18 +129,21 @@ class SideNav extends React.Component {
|
||||
|
||||
tagListComponent () {
|
||||
const { data, location } = this.props
|
||||
const tagList = data.tagNoteMap.map((tag, key) => {
|
||||
return key
|
||||
})
|
||||
const tagList = _.sortBy(data.tagNoteMap.map((tag, name) => {
|
||||
return { name, size: tag.size }
|
||||
}), ['name'])
|
||||
return (
|
||||
tagList.map(tag => (
|
||||
<TagListItem
|
||||
name={tag}
|
||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||
isActive={this.getTagActive(location.pathname, tag)}
|
||||
key={tag}
|
||||
/>
|
||||
))
|
||||
tagList.map(tag => {
|
||||
return (
|
||||
<TagListItem
|
||||
name={tag.name}
|
||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||
isActive={this.getTagActive(location.pathname, tag)}
|
||||
key={tag.name}
|
||||
count={tag.size}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,19 +158,48 @@ class SideNav extends React.Component {
|
||||
router.push(`/tags/${name}`)
|
||||
}
|
||||
|
||||
emptyTrash (entries) {
|
||||
const { dispatch } = this.props
|
||||
const deletionPromises = entries.map((note) => {
|
||||
return dataApi.deleteNote(note.storage, note.key)
|
||||
})
|
||||
Promise.all(deletionPromises)
|
||||
.then((arrayOfStorageAndNoteKeys) => {
|
||||
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Cannot Delete note: ' + err)
|
||||
})
|
||||
console.log('Trash emptied')
|
||||
}
|
||||
|
||||
handleFilterButtonContextMenu (event) {
|
||||
const { data } = this.props
|
||||
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{ label: 'Empty Trash', click: () => this.emptyTrash(trashedNotes) }
|
||||
])
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, location, config, dispatch } = this.props
|
||||
|
||||
const isFolded = config.isSideNavFolded
|
||||
|
||||
const storageList = data.storageMap.map((storage, key) => {
|
||||
return <StorageItem
|
||||
const SortableStorageItem = SortableContainer(StorageItem)
|
||||
return <SortableStorageItem
|
||||
key={storage.key}
|
||||
storage={storage}
|
||||
data={data}
|
||||
location={location}
|
||||
isFolded={isFolded}
|
||||
dispatch={dispatch}
|
||||
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
||||
useDragHandle
|
||||
/>
|
||||
})
|
||||
const style = {}
|
||||
|
||||
@@ -21,20 +21,19 @@
|
||||
color white
|
||||
|
||||
.zoom
|
||||
display none
|
||||
// navButtonColor()
|
||||
// color rgba(0,0,0,.54)
|
||||
// height 20px
|
||||
// display flex
|
||||
// padding 0
|
||||
// align-items center
|
||||
// background-color transparent
|
||||
// &:hover
|
||||
// color $ui-active-color
|
||||
// &:active
|
||||
// color $ui-active-color
|
||||
// span
|
||||
// margin-left 5px
|
||||
navButtonColor()
|
||||
color rgba(0,0,0,.54)
|
||||
height 20px
|
||||
display flex
|
||||
padding 0
|
||||
align-items center
|
||||
background-color transparent
|
||||
&:hover
|
||||
color $ui-active-color
|
||||
&:active
|
||||
color $ui-active-color
|
||||
span
|
||||
margin-left 5px
|
||||
|
||||
.update
|
||||
navButtonColor()
|
||||
|
||||
@@ -40,6 +40,32 @@ $control-height = 34px
|
||||
padding-bottom 2px
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.control-search-input-clear
|
||||
height 16px
|
||||
width 16px
|
||||
position absolute
|
||||
right 40px
|
||||
top 10px
|
||||
z-index 300
|
||||
border none
|
||||
background-color transparent
|
||||
color #999
|
||||
&:hover .control-search-input-clear-tooltip
|
||||
opacity 1
|
||||
|
||||
.control-search-input-clear-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
pointer-events none
|
||||
top 50px
|
||||
left 433px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.control-search-optionList
|
||||
position fixed
|
||||
z-index 200
|
||||
@@ -207,4 +233,4 @@ body[data-theme="solarized-dark"]
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
input
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
@@ -22,14 +22,29 @@ class TopBar extends React.Component {
|
||||
this.focusSearchHandler = () => {
|
||||
this.handleOnSearchFocus()
|
||||
}
|
||||
|
||||
this.codeInitHandler = this.handleCodeInit.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ee.on('top:focus-search', this.focusSearchHandler)
|
||||
ee.on('code:init', this.codeInitHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('top:focus-search', this.focusSearchHandler)
|
||||
ee.off('code:init', this.codeInitHandler)
|
||||
}
|
||||
|
||||
handleSearchClearButton (e) {
|
||||
const { router } = this.context
|
||||
this.setState({
|
||||
search: '',
|
||||
isSearching: false
|
||||
})
|
||||
this.refs.search.childNodes[0].blur
|
||||
router.push('/searched')
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
@@ -39,6 +54,23 @@ class TopBar extends React.Component {
|
||||
isIME: false
|
||||
})
|
||||
|
||||
// Clear search on ESC
|
||||
if (e.keyCode === 27) {
|
||||
return this.handleSearchClearButton(e)
|
||||
}
|
||||
|
||||
// Next note on DOWN key
|
||||
if (e.keyCode === 40) {
|
||||
ee.emit('list:next')
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// Prev note on UP key
|
||||
if (e.keyCode === 38) {
|
||||
ee.emit('list:prior')
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// When the key is an alphabet, del, enter or ctr
|
||||
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
|
||||
this.setState({
|
||||
@@ -73,14 +105,16 @@ class TopBar extends React.Component {
|
||||
|
||||
handleSearchChange (e) {
|
||||
const { router } = this.context
|
||||
const keyword = this.refs.searchInput.value
|
||||
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
||||
router.push('/searched')
|
||||
} else {
|
||||
e.preventDefault()
|
||||
}
|
||||
this.setState({
|
||||
search: this.refs.searchInput.value
|
||||
search: keyword
|
||||
})
|
||||
ee.emit('top:search', keyword)
|
||||
}
|
||||
|
||||
handleSearchFocus (e) {
|
||||
@@ -108,13 +142,19 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
handleOnSearchFocus () {
|
||||
const el = this.refs.search.childNodes[0]
|
||||
if (this.state.isSearching) {
|
||||
this.refs.search.childNodes[0].blur()
|
||||
el.blur()
|
||||
} else {
|
||||
this.refs.search.childNodes[0].focus()
|
||||
el.focus()
|
||||
el.setSelectionRange(0, el.value.length)
|
||||
}
|
||||
}
|
||||
|
||||
handleCodeInit () {
|
||||
ee.emit('top:search', this.refs.searchInput.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, style, location } = this.props
|
||||
return (
|
||||
@@ -140,15 +180,15 @@ class TopBar extends React.Component {
|
||||
type='text'
|
||||
className='searchInput'
|
||||
/>
|
||||
{this.state.search !== '' &&
|
||||
<button styleName='control-search-input-clear'
|
||||
onClick={(e) => this.handleSearchClearButton(e)}
|
||||
>
|
||||
<i className='fa fa-fw fa-times' />
|
||||
<span styleName='control-search-input-clear-tooltip'>Clear Search</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
{this.state.search > 0 &&
|
||||
<button styleName='left-search-clearButton'
|
||||
onClick={(e) => this.handleSearchClearButton(e)}
|
||||
>
|
||||
<i className='fa fa-times' />
|
||||
</button>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{location.pathname === '/trashed' ? ''
|
||||
|
||||
@@ -36,7 +36,8 @@ export const DEFAULT_CONFIG = {
|
||||
displayLineNumbers: true,
|
||||
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
||||
scrollPastEnd: false,
|
||||
type: 'SPLIT'
|
||||
type: 'SPLIT',
|
||||
fetchUrlTitle: true
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
@@ -47,7 +48,16 @@ export const DEFAULT_CONFIG = {
|
||||
latexInlineClose: '$',
|
||||
latexBlockOpen: '$$',
|
||||
latexBlockClose: '$$',
|
||||
scrollPastEnd: false
|
||||
scrollPastEnd: false,
|
||||
smartQuotes: true
|
||||
},
|
||||
blog: {
|
||||
type: 'wordpress', // Available value: wordpress, add more types in the future plz
|
||||
address: 'http://wordpress.com/wp-json',
|
||||
authMethod: 'JWT', // Available value: JWT, USER
|
||||
token: '',
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +160,7 @@ function set (updates) {
|
||||
function assignConfigValues (originalConfig, rcConfig) {
|
||||
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
|
||||
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
|
||||
config.blog = Object.assign({}, DEFAULT_CONFIG.blog, originalConfig.blog, rcConfig.blog)
|
||||
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
|
||||
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
||||
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
||||
|
||||
31
browser/main/lib/dataApi/copyFile.js
Executable file
31
browser/main/lib/dataApi/copyFile.js
Executable file
@@ -0,0 +1,31 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* @description Copy a file from source to destination
|
||||
* @param {String} srcPath
|
||||
* @param {String} dstPath
|
||||
* @return {Promise} an image path
|
||||
*/
|
||||
function copyFile (srcPath, dstPath) {
|
||||
if (!path.extname(dstPath)) {
|
||||
dstPath = path.join(dstPath, path.basename(srcPath))
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const dstFolder = path.dirname(dstPath)
|
||||
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
||||
|
||||
const input = fs.createReadStream(srcPath)
|
||||
const output = fs.createWriteStream(dstPath)
|
||||
|
||||
output.on('error', reject)
|
||||
input.on('error', reject)
|
||||
input.on('end', () => {
|
||||
resolve(dstPath)
|
||||
})
|
||||
input.pipe(output)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = copyFile
|
||||
@@ -3,19 +3,20 @@ const path = require('path')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
/**
|
||||
* @description To copy an image and return the path.
|
||||
* @description Copy an image and return the path.
|
||||
* @param {String} filePath
|
||||
* @param {String} storageKey
|
||||
* @return {String} an image path
|
||||
* @param {Boolean} rename create new filename or leave the old one
|
||||
* @return {Promise<any>} an image path
|
||||
*/
|
||||
function copyImage (filePath, storageKey) {
|
||||
function copyImage (filePath, storageKey, rename = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const targetStorage = findStorage(storageKey)
|
||||
|
||||
const inputImage = fs.createReadStream(filePath)
|
||||
const imageExt = path.extname(filePath)
|
||||
const imageName = Math.random().toString(36).slice(-16)
|
||||
const imageName = rename ? Math.random().toString(36).slice(-16) : path.basename(filePath, imageExt)
|
||||
const basename = `${imageName}${imageExt}`
|
||||
const imageDir = path.join(targetStorage.path, 'images')
|
||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||
|
||||
@@ -52,12 +52,12 @@ function createNote (storageKey, input) {
|
||||
return storage
|
||||
})
|
||||
.then(function saveNote (storage) {
|
||||
let key = keygen()
|
||||
let key = keygen(true)
|
||||
let isUnique = false
|
||||
while (!isUnique) {
|
||||
try {
|
||||
sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
|
||||
key = keygen()
|
||||
key = keygen(true)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
isUnique = true
|
||||
|
||||
110
browser/main/lib/dataApi/exportNote.js
Executable file
110
browser/main/lib/dataApi/exportNote.js
Executable file
@@ -0,0 +1,110 @@
|
||||
import copyFile from 'browser/main/lib/dataApi/copyFile'
|
||||
import {findStorage} from 'browser/lib/findStorage'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const LOCAL_STORED_REGEX = /!\[(.*?)]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
||||
const IMAGES_FOLDER_NAME = 'images'
|
||||
|
||||
/**
|
||||
* Export note together with images
|
||||
*
|
||||
* If images is stored in the storage, creates 'images' subfolder in target directory
|
||||
* and copies images to it. Changes links to images in the content of the note
|
||||
*
|
||||
* @param {String} storageKey or storage path
|
||||
* @param {String} noteContent Content to export
|
||||
* @param {String} targetPath Path to exported file
|
||||
* @param {function} outputFormatter
|
||||
* @return {Promise.<*[]>}
|
||||
*/
|
||||
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
||||
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
||||
const exportTasks = []
|
||||
|
||||
if (!storagePath) {
|
||||
throw new Error('Storage path is not found')
|
||||
}
|
||||
|
||||
let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => {
|
||||
if (!path.extname(dstFilename)) {
|
||||
dstFilename += path.extname(srcFilename)
|
||||
}
|
||||
|
||||
const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename)
|
||||
|
||||
exportTasks.push({
|
||||
src: path.join(IMAGES_FOLDER_NAME, srcFilename),
|
||||
dst: dstRelativePath
|
||||
})
|
||||
|
||||
return ``
|
||||
})
|
||||
|
||||
if (outputFormatter) {
|
||||
exportedData = outputFormatter(exportedData, exportTasks)
|
||||
}
|
||||
|
||||
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
||||
|
||||
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
|
||||
.then(() => {
|
||||
return saveToFile(exportedData, targetPath)
|
||||
}).catch((err) => {
|
||||
rollbackExport(tasks)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
function prepareTasks (tasks, storagePath, targetPath) {
|
||||
return tasks.map((task) => {
|
||||
if (!path.isAbsolute(task.src)) {
|
||||
task.src = path.join(storagePath, task.src)
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(task.dst)) {
|
||||
task.dst = path.join(targetPath, task.dst)
|
||||
}
|
||||
|
||||
return task
|
||||
})
|
||||
}
|
||||
|
||||
function saveToFile (data, filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(filename, data, (err) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
resolve(filename)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove exported files
|
||||
* @param tasks Array of copy task objects. Object consists of two mandatory fields – `src` and `dst`
|
||||
*/
|
||||
function rollbackExport (tasks) {
|
||||
const folders = new Set()
|
||||
tasks.forEach((task) => {
|
||||
let fullpath = task.dst
|
||||
|
||||
if (!path.extname(task.dst)) {
|
||||
fullpath = path.join(task.dst, path.basename(task.src))
|
||||
}
|
||||
|
||||
if (fs.existsSync(fullpath)) {
|
||||
fs.unlink(fullpath)
|
||||
folders.add(path.dirname(fullpath))
|
||||
}
|
||||
})
|
||||
|
||||
folders.forEach((folder) => {
|
||||
if (fs.readdirSync(folder).length === 0) {
|
||||
fs.rmdir(folder)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default exportNote
|
||||
@@ -1,10 +1,12 @@
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const sander = require('sander')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
const copyImage = require('./copyImage')
|
||||
|
||||
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||
let oldStorage, newStorage
|
||||
@@ -37,12 +39,12 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||
return resolveStorageData(newStorage)
|
||||
.then(function findNewNoteKey (_newStorage) {
|
||||
newStorage = _newStorage
|
||||
newNoteKey = keygen()
|
||||
newNoteKey = keygen(true)
|
||||
let isUnique = false
|
||||
while (!isUnique) {
|
||||
try {
|
||||
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
|
||||
newNoteKey = keygen()
|
||||
newNoteKey = keygen(true)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
isUnique = true
|
||||
@@ -65,6 +67,27 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||
|
||||
return noteData
|
||||
})
|
||||
.then(function moveImages (noteData) {
|
||||
const searchImagesRegex = /!\[.*?]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
||||
let match = searchImagesRegex.exec(noteData.content)
|
||||
|
||||
const moveTasks = []
|
||||
while (match != null) {
|
||||
const [, filename] = match
|
||||
const oldPath = path.join(oldStorage.path, 'images', filename)
|
||||
moveTasks.push(
|
||||
copyImage(oldPath, noteData.storage, false)
|
||||
.then(() => {
|
||||
fs.unlinkSync(oldPath)
|
||||
})
|
||||
)
|
||||
|
||||
// find next occurence
|
||||
match = searchImagesRegex.exec(noteData.content)
|
||||
}
|
||||
|
||||
return Promise.all(moveTasks).then(() => noteData)
|
||||
})
|
||||
.then(function writeAndReturn (noteData) {
|
||||
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
||||
return noteData
|
||||
|
||||
@@ -30,6 +30,9 @@ function validateInput (input) {
|
||||
validatedInput.isPinned = !!input.isPinned
|
||||
}
|
||||
|
||||
if (!_.isNil(input.blog)) {
|
||||
validatedInput.blog = input.blog
|
||||
}
|
||||
validatedInput.type = input.type
|
||||
switch (input.type) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
|
||||
@@ -18,7 +18,7 @@ nodeIpc.connectTo(
|
||||
console.log(err)
|
||||
})
|
||||
nodeIpc.of.node.on('connect', function () {
|
||||
console.log('Conncted successfully')
|
||||
console.log('Connected successfully')
|
||||
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
|
||||
})
|
||||
nodeIpc.of.node.on('disconnect', function () {
|
||||
|
||||
@@ -35,14 +35,16 @@ class NewNoteModal extends React.Component {
|
||||
content: ''
|
||||
})
|
||||
.then((note) => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: note.storage + '-' + note.key}
|
||||
query: {key: noteHash}
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
this.props.close()
|
||||
})
|
||||
@@ -73,14 +75,16 @@ class NewNoteModal extends React.Component {
|
||||
}]
|
||||
})
|
||||
.then((note) => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: note.storage + '-' + note.key}
|
||||
query: {key: noteHash}
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
this.props.close()
|
||||
})
|
||||
|
||||
198
browser/main/modals/PreferencesModal/Blog.js
Normal file
198
browser/main/modals/PreferencesModal/Blog.js
Normal file
@@ -0,0 +1,198 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ConfigTab.styl'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import store from 'browser/main/store'
|
||||
import PropTypes from 'prop-types'
|
||||
import _ from 'lodash'
|
||||
|
||||
const electron = require('electron')
|
||||
const { shell } = electron
|
||||
const ipc = electron.ipcRenderer
|
||||
class Blog extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
config: props.config,
|
||||
BlogAlert: null
|
||||
}
|
||||
}
|
||||
|
||||
handleLinkClick (e) {
|
||||
shell.openExternal(e.currentTarget.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
clearMessage () {
|
||||
_.debounce(() => {
|
||||
this.setState({
|
||||
BlogAlert: null
|
||||
})
|
||||
}, 2000)()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.handleSettingDone = () => {
|
||||
this.setState({BlogAlert: {
|
||||
type: 'success',
|
||||
message: 'Successfully applied!'
|
||||
}})
|
||||
}
|
||||
this.handleSettingError = (err) => {
|
||||
this.setState({BlogAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : 'Error occurs!'
|
||||
}})
|
||||
}
|
||||
this.oldBlog = this.state.config.blog
|
||||
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
|
||||
}
|
||||
|
||||
handleBlogChange (e) {
|
||||
const { config } = this.state
|
||||
config.blog = {
|
||||
password: !_.isNil(this.refs.passwordInput) ? this.refs.passwordInput.value : config.blog.password,
|
||||
username: !_.isNil(this.refs.usernameInput) ? this.refs.usernameInput.value : config.blog.username,
|
||||
token: !_.isNil(this.refs.tokenInput) ? this.refs.tokenInput.value : config.blog.token,
|
||||
authMethod: this.refs.authMethodDropdown.value,
|
||||
address: this.refs.addressInput.value,
|
||||
type: this.refs.typeDropdown.value
|
||||
}
|
||||
this.setState({
|
||||
config
|
||||
})
|
||||
if (_.isEqual(this.oldBlog, config.blog)) {
|
||||
this.props.haveToSave()
|
||||
} else {
|
||||
this.props.haveToSave({
|
||||
tab: 'Blog',
|
||||
type: 'warning',
|
||||
message: 'You have to save!'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveButtonClick (e) {
|
||||
const newConfig = {
|
||||
blog: this.state.config.blog
|
||||
}
|
||||
|
||||
ConfigManager.set(newConfig)
|
||||
|
||||
store.dispatch({
|
||||
type: 'SET_UI',
|
||||
config: newConfig
|
||||
})
|
||||
this.clearMessage()
|
||||
this.props.haveToSave()
|
||||
}
|
||||
|
||||
render () {
|
||||
const {config, BlogAlert} = this.state
|
||||
const blogAlertElement = BlogAlert != null
|
||||
? <p className={`alert ${BlogAlert.type}`}>
|
||||
{BlogAlert.message}
|
||||
</p>
|
||||
: null
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='group'>
|
||||
<div styleName='group-header'>Blog</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Blog Type
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select
|
||||
value={config.blog.type}
|
||||
ref='typeDropdown'
|
||||
onChange={(e) => this.handleBlogChange(e)}
|
||||
>
|
||||
<option value='wordpress' key='wordpress'>wordpress</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>Blog Address</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleBlogChange(e)}
|
||||
ref='addressInput'
|
||||
value={config.blog.address}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-control'>
|
||||
<button styleName='group-control-rightButton'
|
||||
onClick={(e) => this.handleSaveButtonClick(e)}>Save
|
||||
</button>
|
||||
{blogAlertElement}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-header2'>Auth</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Authentication Method
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select
|
||||
value={config.blog.authMethod}
|
||||
ref='authMethodDropdown'
|
||||
onChange={(e) => this.handleBlogChange(e)}
|
||||
>
|
||||
<option value='JWT' key='JWT'>JWT</option>
|
||||
<option value='USER' key='USER'>USER</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{ config.blog.authMethod === 'JWT' &&
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>Token</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleBlogChange(e)}
|
||||
ref='tokenInput'
|
||||
value={config.blog.token}
|
||||
type='text' />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{ config.blog.authMethod === 'USER' &&
|
||||
<div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>UserName</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleBlogChange(e)}
|
||||
ref='usernameInput'
|
||||
value={config.blog.username}
|
||||
type='text' />
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>Password</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleBlogChange(e)}
|
||||
ref='passwordInput'
|
||||
value={config.blog.password}
|
||||
type='password' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Blog.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
haveToSave: PropTypes.func
|
||||
}
|
||||
|
||||
export default CSSModules(Blog, styles)
|
||||
@@ -42,7 +42,7 @@ class InfoTab extends React.Component {
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
amaMessage: 'Thank\'s for trust us'
|
||||
amaMessage: 'Thanks for trusting us'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class InfoTab extends React.Component {
|
||||
>GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://medium.com/boostnote'
|
||||
<a href='https://boostlog.io/@junp1234'
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
>Blog</a>
|
||||
</li>
|
||||
@@ -128,7 +128,7 @@ class InfoTab extends React.Component {
|
||||
>Development</a> : Development configurations for Boostnote.
|
||||
</li>
|
||||
<li styleName='cc'>
|
||||
Copyright (C) 2017 Maisin&Co.
|
||||
Copyright (C) 2017 - 2018 BoostIO
|
||||
</li>
|
||||
<li styleName='cc'>
|
||||
License: GPL v3
|
||||
|
||||
@@ -76,7 +76,8 @@ class UiTab extends React.Component {
|
||||
displayLineNumbers: this.refs.editorDisplayLineNumbers.checked,
|
||||
switchPreview: this.refs.editorSwitchPreview.value,
|
||||
keyMap: this.refs.editorKeyMap.value,
|
||||
scrollPastEnd: this.refs.scrollPastEnd.checked
|
||||
scrollPastEnd: this.refs.scrollPastEnd.checked,
|
||||
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked
|
||||
},
|
||||
preview: {
|
||||
fontSize: this.refs.previewFontSize.value,
|
||||
@@ -87,7 +88,8 @@ class UiTab extends React.Component {
|
||||
latexInlineClose: this.refs.previewLatexInlineClose.value,
|
||||
latexBlockOpen: this.refs.previewLatexBlockOpen.value,
|
||||
latexBlockClose: this.refs.previewLatexBlockClose.value,
|
||||
scrollPastEnd: this.refs.previewScrollPastEnd.checked
|
||||
scrollPastEnd: this.refs.previewScrollPastEnd.checked,
|
||||
smartQuotes: this.refs.previewSmartQuotes.checked
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +285,7 @@ class UiTab extends React.Component {
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
>
|
||||
<option value='BLUR'>When Editor Blurred</option>
|
||||
<option value='DBL_CLICK'>When Editor Blurred, Edit On Double Click</option>
|
||||
<option value='RIGHTCLICK'>On Right Click</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -327,6 +330,17 @@ class UiTab extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.fetchUrlTitle}
|
||||
ref='editorFetchUrlTitle'
|
||||
type='checkbox'
|
||||
/>
|
||||
Bring in web page title when pasting URL on editor
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-header2'>Preview</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
@@ -389,6 +403,16 @@ class UiTab extends React.Component {
|
||||
Show line numbers for preview code blocks
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.preview.smartQuotes}
|
||||
ref='previewSmartQuotes'
|
||||
type='checkbox'
|
||||
/>
|
||||
Enable smart quotes
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
LaTeX Inline Open Delimiter
|
||||
|
||||
@@ -6,6 +6,7 @@ import UiTab from './UiTab'
|
||||
import InfoTab from './InfoTab'
|
||||
import Crowdfunding from './Crowdfunding'
|
||||
import StoragesTab from './StoragesTab'
|
||||
import Blog from './Blog'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './PreferencesModal.styl'
|
||||
@@ -19,7 +20,8 @@ class Preferences extends React.Component {
|
||||
this.state = {
|
||||
currentTab: 'STORAGES',
|
||||
UIAlert: '',
|
||||
HotkeyAlert: ''
|
||||
HotkeyAlert: '',
|
||||
BlogAlert: ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +77,14 @@ class Preferences extends React.Component {
|
||||
return (
|
||||
<Crowdfunding />
|
||||
)
|
||||
case 'BLOG':
|
||||
return (
|
||||
<Blog
|
||||
dispatch={dispatch}
|
||||
config={config}
|
||||
haveToSave={alert => this.setState({BlogAlert: alert})}
|
||||
/>
|
||||
)
|
||||
case 'STORAGES':
|
||||
default:
|
||||
return (
|
||||
@@ -111,7 +121,8 @@ class Preferences extends React.Component {
|
||||
{target: 'HOTKEY', label: 'Hotkeys', Hotkey: this.state.HotkeyAlert},
|
||||
{target: 'UI', label: 'Interface', UI: this.state.UIAlert},
|
||||
{target: 'INFO', label: 'About'},
|
||||
{target: 'CROWDFUNDING', label: 'Crowdfunding'}
|
||||
{target: 'CROWDFUNDING', label: 'Crowdfunding'},
|
||||
{target: 'BLOG', label: 'Blog', Blog: this.state.BlogAlert}
|
||||
]
|
||||
|
||||
const navButtons = tabs.map((tab) => {
|
||||
|
||||
@@ -27,7 +27,7 @@ function data (state = defaultDataMap(), action) {
|
||||
|
||||
action.notes.some((note) => {
|
||||
if (note === undefined) return true
|
||||
const uniqueKey = note.storage + '-' + note.key
|
||||
const uniqueKey = note.key
|
||||
const folderKey = note.storage + '-' + note.folder
|
||||
state.noteMap.set(uniqueKey, note)
|
||||
|
||||
@@ -66,7 +66,7 @@ function data (state = defaultDataMap(), action) {
|
||||
case 'UPDATE_NOTE':
|
||||
{
|
||||
const note = action.note
|
||||
const uniqueKey = note.storage + '-' + note.key
|
||||
const uniqueKey = note.key
|
||||
const folderKey = note.storage + '-' + note.folder
|
||||
const oldNote = state.noteMap.get(uniqueKey)
|
||||
|
||||
@@ -162,9 +162,9 @@ function data (state = defaultDataMap(), action) {
|
||||
case 'MOVE_NOTE':
|
||||
{
|
||||
const originNote = action.originNote
|
||||
const originKey = originNote.storage + '-' + originNote.key
|
||||
const originKey = originNote.key
|
||||
const note = action.note
|
||||
const uniqueKey = note.storage + '-' + note.key
|
||||
const uniqueKey = note.key
|
||||
const folderKey = note.storage + '-' + note.folder
|
||||
const oldNote = state.noteMap.get(uniqueKey)
|
||||
|
||||
@@ -297,7 +297,7 @@ function data (state = defaultDataMap(), action) {
|
||||
}
|
||||
case 'DELETE_NOTE':
|
||||
{
|
||||
const uniqueKey = action.storageKey + '-' + action.noteKey
|
||||
const uniqueKey = action.noteKey
|
||||
const targetNote = state.noteMap.get(uniqueKey)
|
||||
|
||||
state = Object.assign({}, state)
|
||||
@@ -423,7 +423,7 @@ function data (state = defaultDataMap(), action) {
|
||||
state.folderNoteMap = new Map(state.folderNoteMap)
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
action.notes.forEach((note) => {
|
||||
const uniqueKey = note.storage + '-' + note.key
|
||||
const uniqueKey = note.key
|
||||
const folderKey = note.storage + '-' + note.folder
|
||||
state.noteMap.set(uniqueKey, note)
|
||||
|
||||
@@ -483,7 +483,7 @@ function data (state = defaultDataMap(), action) {
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
state.starredSet = new Set(state.starredSet)
|
||||
notes.forEach((note) => {
|
||||
const noteKey = storage.key + '-' + note.key
|
||||
const noteKey = note.key
|
||||
state.noteMap.delete(noteKey)
|
||||
state.starredSet.delete(noteKey)
|
||||
note.tags.forEach((tag) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ $danger-color = #c9302c
|
||||
$danger-lighten-color = lighten(#c9302c, 5%)
|
||||
|
||||
// Layouts
|
||||
$statusBar-height = 0px
|
||||
$statusBar-height = 22px
|
||||
$sideNav-width = 200px
|
||||
$sideNav--folded-width = 44px
|
||||
$topBar-height = 60px
|
||||
@@ -347,4 +347,4 @@ modalSolarizedDark()
|
||||
width 100%
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
overflow hidden
|
||||
border-radius $modal-border-radius
|
||||
border-radius $modal-border-radius
|
||||
|
||||
@@ -9,7 +9,7 @@ Thank you for your help in advance.
|
||||
|
||||
### About copyright of Pull Request
|
||||
|
||||
If you make a pull request, It means you agree to transfer the copyright of the code changes to Maisin&Co.
|
||||
If you make a pull request, It means you agree to transfer the copyright of the code changes to BoostIO.
|
||||
|
||||
It doesn't mean Boostnote will become a paid app. If we want to earn some money, We will try other way, which is some kind of cloud storage, Mobile app integration or some SPECIAL features.
|
||||
Because GPL v3 is too strict to be compatible with any other License, We thought this is needed to replace the license with much freer one(like BSD, MIT) somewhen.
|
||||
@@ -27,7 +27,7 @@ Because GPL v3 is too strict to be compatible with any other License, We thought
|
||||
|
||||
### Об авторских правах Pull Request
|
||||
|
||||
Если вы делаете pull request, значит вы согласны передать авторские права на изменения кода в Maisin&Co.
|
||||
Если вы делаете pull request, значит вы согласны передать авторские права на изменения кода в BoostIO.
|
||||
|
||||
Это не означает, что Boostnote станет платным приложением. Если мы захотим заработать немного денег, мы найдем другой способ. Например, использование облачного хранилища, интеграцией мобильных приложений или другими специальными функциями.
|
||||
Так как лицензия GPL v3 слишком строгая, чтобы быть совместимой с любой другой лицензией, мы думаем, что нужно заменить лицензию на более свободную (например, BSD, MIT).
|
||||
@@ -45,7 +45,7 @@ Because GPL v3 is too strict to be compatible with any other License, We thought
|
||||
|
||||
### Pull Request의 저작권에 관하여
|
||||
|
||||
당신이 pull request를 요청하면, 코드 변경에 대한 저작권을 Maisin&Co에 양도한다는 것에 동의한다는 의미입니다.
|
||||
당신이 pull request를 요청하면, 코드 변경에 대한 저작권을 BoostIO에 양도한다는 것에 동의한다는 의미입니다.
|
||||
|
||||
이것은 Boostnote가 유료화가 되는 것을 의미하는 건 아닙니다. 만약 우리가 자금이 필요하다면, 우리는 클라우드 연동, 모바일 앱 통합 혹은 특수한 기능 같은 것을 사용해 수입 창출을 시도할 것입니다.
|
||||
GPL v3 라이센스는 다른 라이센스와 혼합해 사용하기엔 너무 엄격하므로, 우리는 BSD, MIT 라이센스와 같은 더 자유로운 라이센스로 교체하는 것을 생각하고 있습니다.
|
||||
@@ -63,7 +63,7 @@ GPL v3 라이센스는 다른 라이센스와 혼합해 사용하기엔 너무
|
||||
|
||||
### Pull requestの著作権について
|
||||
|
||||
Pull requestをすることはその変化分のコードの著作権をMaisin&Co.に譲渡することに同意することになります。
|
||||
Pull requestをすることはその変化分のコードの著作権をBoostIOに譲渡することに同意することになります。
|
||||
|
||||
アプリケーションのLicenseをいつでも変える選択肢を残したいと思うからです。
|
||||
これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。
|
||||
@@ -83,7 +83,7 @@ Pull requestをすることはその変化分のコードの著作権をMaisin&C
|
||||
感谢您对我们的支持。
|
||||
|
||||
### 关于您提供的Pull Request的著作权(版权)问题
|
||||
如果您提供了一个Pull Request,这表示您将您所修改的代码的著作权移交给Maisin&Co。
|
||||
如果您提供了一个Pull Request,这表示您将您所修改的代码的著作权移交给BoostIO。
|
||||
|
||||
这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益,我们会尝试一些其他的方法,比如说云存储、绑定手机软件等。
|
||||
因为GPLv3过于严格,不能和其他的一些协议兼容,所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议,比如说BSD、MIT。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build
|
||||
Diese Seite ist auch verfügbar in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
|
||||
Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
|
||||
|
||||
## Umgebungen
|
||||
* npm: 4.x
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
# How to debug Boostnote (Electron app)
|
||||
Diese Seite ist auch verfügbar in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) and [German](https://github.com/BoostIO/Boostnote/blob/add-german-documents/docs/de/debug.md).
|
||||
|
||||
Boostnote is eine Electron app, somit basiert sie auf Chromium; Entwickler können die `Developer Tools` verwenden, wie Google Chrome.
|
||||
Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md).
|
||||
|
||||
|
||||
Boostnote ist eine Electron App und basiert auf Chromium.
|
||||
|
||||
Zum Entwicklen verwendest du am Besten die `Developer Tools` von Google Chrome verwenden. Diese kannst du ganz einfach im unter dem Menüpunkt `View` mit `Toggle Developer Tools` aktivieren:
|
||||
|
||||
Du kannst die `Developer Tools` so einschalten:
|
||||

|
||||
|
||||
Die `Developer Tools` schauen dann ungefähr so aus:
|
||||
Die Anzeige der `Developer Tools` sieht in etwa so aus:
|
||||
|
||||

|
||||
|
||||
Wenn Fehler vorkommen, werden die Fehlermeldungen in der `console` ausgegeben.
|
||||
|
||||
## Debugging
|
||||
Zum Beispiel kannst du mit dem `debugger` Haltepunkte im Code setzen wie hier veranschaulicht:
|
||||
|
||||
Fehlermeldungen werden in der Regel in der `console` ausgegeben, die du über den gleichnamigen Reiter der `Developer Tools` anzeigen lassen kannst. Zum Debuggen kannst du beispielsweise über den `debugger` Haltepunkte im Code setzen.
|
||||
|
||||

|
||||
|
||||
Das ist ledigtlich ein Beispiel, du kannst die Art von Debugging verwenden die für dich am besten ist.
|
||||
Du kannst aber natürlich auch die Art von Debugging verwenden mit der du am besten zurecht kommst.
|
||||
|
||||
## Referenz
|
||||
|
||||
* [Official document of Google Chrome about debugging](https://developer.chrome.com/devtools)
|
||||
|
||||
---
|
||||
|
||||
Special thanks: Translated by [gino909](https://github.com/gino909)
|
||||
Special thanks: Translated by [gino909](https://github.com/gino909), [mdeuerlein](https://github.com/mdeuerlein)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# How to debug Boostnote (Electron app)
|
||||
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) and [German](https://github.com/BoostIO/Boostnote/blob/add-german-documents/docs/de/debug.md).
|
||||
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md).
|
||||
|
||||
Boostnote is an Electron app so it's based on Chromium; developers can use `Developer Tools` just like Google Chrome.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build
|
||||
Cette page est également disponible en [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), et en [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md).
|
||||
Cette page est également disponible en [Angalis](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md), [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md) et en [Allemand](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md)
|
||||
|
||||
## Environnements
|
||||
* npm: 4.x
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Comment débugger Boostnote (Application Electron)
|
||||
Cette page est également disponible en [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), et en [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md)
|
||||
Cette page est également disponible en [Angalis](https://github.com/BoostIO/Boostnote/blob/master/docs/debug.md), [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md) et en [Allemand](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md)
|
||||
|
||||
Boostnote est une application Electron donc basée sur Chromium. Il est possible d'utiliser les `Developer Tools` comme dans Google Chrome.
|
||||
|
||||
@@ -19,4 +19,4 @@ Par exemple, vous pouvez utiliser le `debugger` pour placer un point d'arrêt da
|
||||
C'est une façon comme une autre de faire, vous pouvez trouver une façon de débugger que vous trouverez plus adaptée.
|
||||
|
||||
## Références
|
||||
* [Documentation officiel de Google Chrome sur le debugging](https://developer.chrome.com/devtools)
|
||||
* [Documentation officiel de Google Chrome sur le debugging](https://developer.chrome.com/devtools)
|
||||
|
||||
86
docs/zh_TW/build.md
Normal file
86
docs/zh_TW/build.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 編譯
|
||||
此文件還提供下列的語言 [日文](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [韓文](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [俄文](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [簡體中文](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [法文](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [德文](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
|
||||
|
||||
## 環境
|
||||
* npm: 4.x
|
||||
* node: 7.x
|
||||
|
||||
`$ grunt pre-build` 在 `npm v5.x` 有問題,所以只能用 `npm v4.x` 。
|
||||
|
||||
## 開發
|
||||
|
||||
我們使用 Webpack HMR 來開發 Boostnote。
|
||||
|
||||
在專案根目錄底下執行下列指令,將會以原始設置啟動 Boostnote。
|
||||
|
||||
**用 yarn 來安裝必要 packages**
|
||||
|
||||
```bash
|
||||
$ yarn
|
||||
```
|
||||
|
||||
**開始開發**
|
||||
|
||||
```
|
||||
$ yarn run dev-start
|
||||
```
|
||||
|
||||
上述指令同時運行了 `yarn run webpack` 及 `yarn run hot`,相當於將這兩個指令在不同的 terminal 中運行。
|
||||
|
||||
`webpack` 會同時監控修改過的程式碼,並
|
||||
The `webpack` will watch for code changes and then apply them automatically.
|
||||
|
||||
If the following error occurs: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, please reload Boostnote.
|
||||
|
||||

|
||||
|
||||
> ### Notice
|
||||
> There are some cases where you have to refresh the app manually.
|
||||
> 1. When editing a constructor method of a component
|
||||
> 2. When adding a new css class (similar to 1: the CSS class is re-written by each component. This process occurs at the Constructor method.)
|
||||
|
||||
## Deploy
|
||||
|
||||
We use Grunt to automate deployment.
|
||||
You can build the program by using `grunt`. However, we don't recommend this because the default task includes codesign and authenticode.
|
||||
|
||||
So, we've prepared a separate script which just makes an executable file.
|
||||
|
||||
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
|
||||
|
||||
```
|
||||
grunt pre-build
|
||||
```
|
||||
|
||||
You will find the executable in the `dist` directory. Note, the auto updater won't work because the app isn't signed.
|
||||
|
||||
If you find it necessary, you can use codesign or authenticode with this executable.
|
||||
|
||||
## Make own distribution packages (deb, rpm)
|
||||
|
||||
Distribution packages are created by exec `grunt build` on Linux platform (e.g. Ubuntu, Fedora).
|
||||
|
||||
> Note: You can create both `.deb` and `.rpm` in a single environment.
|
||||
|
||||
After installing the supported version of `node` and `npm`, install build dependency packages.
|
||||
|
||||
|
||||
Ubuntu/Debian:
|
||||
|
||||
```
|
||||
$ sudo apt-get install -y rpm fakeroot
|
||||
```
|
||||
|
||||
Fedora:
|
||||
|
||||
```
|
||||
$ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot
|
||||
```
|
||||
|
||||
Then execute `grunt build`.
|
||||
|
||||
```
|
||||
$ grunt build
|
||||
```
|
||||
|
||||
You will find `.deb` and `.rpm` in the `dist` directory.
|
||||
@@ -0,0 +1,52 @@
|
||||
(function (mod) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') { // Common JS
|
||||
mod(require('../codemirror/lib/codemirror'))
|
||||
} else if (typeof define === 'function' && define.amd) { // AMD
|
||||
define(['../codemirror/lib/codemirror'], mod)
|
||||
} else { // Plain browser env
|
||||
mod(CodeMirror)
|
||||
}
|
||||
})(function (CodeMirror) {
|
||||
'use strict'
|
||||
|
||||
var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/
|
||||
var emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/
|
||||
var unorderedListRE = /[*+-]\s/
|
||||
|
||||
CodeMirror.commands.boostNewLineAndIndentContinueMarkdownList = function (cm) {
|
||||
if (cm.getOption('disableInput')) return CodeMirror.Pass
|
||||
var ranges = cm.listSelections()
|
||||
var replacements = []
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var pos = ranges[i].head
|
||||
var eolState = cm.getStateAfter(pos.line)
|
||||
var inList = eolState.list !== false
|
||||
var inQuote = eolState.quote !== 0
|
||||
var line = cm.getLine(pos.line)
|
||||
var match = listRE.exec(line)
|
||||
if (!ranges[i].empty() || (!inList && !inQuote) || !match || pos.ch < match[2].length - 1) {
|
||||
cm.execCommand('newlineAndIndent')
|
||||
return
|
||||
}
|
||||
if (emptyListRE.test(line)) {
|
||||
if (!/>\s*$/.test(line)) {
|
||||
cm.replaceRange('', {
|
||||
line: pos.line, ch: 0
|
||||
}, {
|
||||
line: pos.line, ch: pos.ch + 1
|
||||
})
|
||||
}
|
||||
replacements[i] = '\n'
|
||||
} else {
|
||||
var indent = match[1]
|
||||
var after = match[5]
|
||||
var bullet = unorderedListRE.test(match[2]) || match[2].indexOf('>') >= 0
|
||||
? match[2].replace('x', ' ')
|
||||
: (parseInt(match[3], 10) + 1) + match[4]
|
||||
replacements[i] = '\n' + indent + bullet + after
|
||||
}
|
||||
}
|
||||
|
||||
cm.replaceSelections(replacements)
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
const electron = require('electron')
|
||||
const BrowserWindow = electron.BrowserWindow
|
||||
const shell = electron.shell
|
||||
const ipc = electron.ipcMain
|
||||
const mainWindow = require('./main-window')
|
||||
|
||||
const macOS = process.platform === 'darwin'
|
||||
@@ -259,6 +260,23 @@ const view = {
|
||||
]
|
||||
}
|
||||
|
||||
let editorFocused
|
||||
|
||||
// Define extra shortcut keys
|
||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||
// Synonyms for Search (Find)
|
||||
if (input.control && input.key === 'f' && input.type === 'keyDown') {
|
||||
if (!editorFocused) {
|
||||
mainWindow.webContents.send('top:focus-search')
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipc.on('editor:focused', (event, isFocused) => {
|
||||
editorFocused = isFocused
|
||||
})
|
||||
|
||||
const window = {
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
@@ -267,6 +285,11 @@ const window = {
|
||||
accelerator: 'Command+M',
|
||||
selector: 'performMiniaturize:'
|
||||
},
|
||||
{
|
||||
label: 'Toggle Full Screen',
|
||||
accelerator: 'Command+Control+F',
|
||||
selector: 'toggleFullScreen:'
|
||||
},
|
||||
{
|
||||
label: 'Close',
|
||||
accelerator: 'Command+W',
|
||||
|
||||
@@ -40,20 +40,16 @@ mainWindow.webContents.sendInputEvent({
|
||||
keyCode: '\u0008'
|
||||
})
|
||||
|
||||
if (process.platform !== 'linux' || process.env.DESKTOP_SESSION === 'cinnamon') {
|
||||
if (process.platform === 'darwin' || process.env.DESKTOP_SESSION === 'cinnamon') {
|
||||
mainWindow.on('close', function (e) {
|
||||
e.preventDefault()
|
||||
if (process.platform === 'win32') {
|
||||
quitApp()
|
||||
} else {
|
||||
if (mainWindow.isFullScreen()) {
|
||||
mainWindow.once('leave-full-screen', function () {
|
||||
mainWindow.hide()
|
||||
})
|
||||
mainWindow.setFullScreen(false)
|
||||
} else {
|
||||
if (mainWindow.isFullScreen()) {
|
||||
mainWindow.once('leave-full-screen', function () {
|
||||
mainWindow.hide()
|
||||
}
|
||||
})
|
||||
mainWindow.setFullScreen(false)
|
||||
} else {
|
||||
mainWindow.hide()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -66,9 +62,6 @@ if (process.platform !== 'linux' || process.env.DESKTOP_SESSION === 'cinnamon')
|
||||
})
|
||||
}
|
||||
mainWindow.on('resize', _.throttle(storeWindowSize, 500))
|
||||
function quitApp () {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
function storeWindowSize () {
|
||||
try {
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<script src="../node_modules/codemirror/keymap/emacs.js"></script>
|
||||
<script src="../node_modules/codemirror/addon/runmode/runmode.js"></script>
|
||||
|
||||
<script src="../node_modules/codemirror/addon/edit/continuelist.js"></script>
|
||||
<script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script>
|
||||
|
||||
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "boost",
|
||||
"productName": "Boostnote",
|
||||
"version": "0.9.0",
|
||||
"version": "0.11.2",
|
||||
"main": "index.js",
|
||||
"description": "Boostnote",
|
||||
"license": "GPL-3.0",
|
||||
@@ -58,9 +58,10 @@
|
||||
"electron-gh-releases": "^2.0.2",
|
||||
"flowchart.js": "^1.6.5",
|
||||
"font-awesome": "^4.3.0",
|
||||
"iconv-lite": "^0.4.19",
|
||||
"immutable": "^3.8.1",
|
||||
"js-sequence-diagrams": "^1000000.0.6",
|
||||
"katex": "^0.8.3",
|
||||
"katex": "^0.9.0",
|
||||
"lodash": "^4.11.1",
|
||||
"lodash-move": "^1.1.1",
|
||||
"markdown-it": "^6.0.1",
|
||||
@@ -84,9 +85,11 @@
|
||||
"react-sortable-hoc": "^0.6.7",
|
||||
"redux": "^3.5.2",
|
||||
"sander": "^0.5.1",
|
||||
"sanitize-html": "^1.18.2",
|
||||
"striptags": "^2.2.1",
|
||||
"superagent": "^1.2.0",
|
||||
"superagent-promise": "^1.0.3"
|
||||
"superagent-promise": "^1.0.3",
|
||||
"uuid": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^0.16.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
:mega: Open sourcing our [Android and iOS apps](https://github.com/BoostIO/Boostnote-mobile)!
|
||||
:mega: We've launched a blogging platform with markdown called **[Boostlog](https://boostlog.io/)**.
|
||||
|
||||

|
||||
|
||||
@@ -25,8 +25,8 @@ Boostnote is an open source project. It's an independent project with its ongoin
|
||||
## Community
|
||||
- [Facebook Group](https://www.facebook.com/groups/boostnote/)
|
||||
- [Twitter](https://twitter.com/boostnoteapp)
|
||||
- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzAzMjI1MTIyNTQ3LTc2MjNiYWU3NTc1YjZlMTk3NzFmOWE1ZWU1MGRhMzBkMGIwMWFjOWMxMDRiM2I2NzkzYzc4OGZhNmVhZjYzZTM)
|
||||
- [Blog](https://medium.com/boostnote)
|
||||
- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzI3NTIxMTQzMTQzLTUyYWZmZWM1YzcwYzQ5OWQ5YzA3Y2M2NzUzNmIwNzYzMjg5NmQyOGJlNzcyZDJhMGY0ZDc0ZjdlZDFhMDdiMWE)
|
||||
- [Blog](https://boostlog.io/tags/boostnote)
|
||||
- [Reddit](https://www.reddit.com/r/Boostnote/)
|
||||
|
||||
|
||||
|
||||
12
tests/fixtures/TestDummy.js
vendored
12
tests/fixtures/TestDummy.js
vendored
@@ -22,7 +22,7 @@ function dummyBoostnoteJSONData (override = {}, isLegacy = false) {
|
||||
if (override.folders == null) {
|
||||
data.folders = []
|
||||
|
||||
var folderCount = Math.floor((Math.random() * 5)) + 1
|
||||
var folderCount = Math.floor((Math.random() * 5)) + 2
|
||||
for (var i = 0; i < folderCount; i++) {
|
||||
var key = keygen()
|
||||
while (data.folders.some((folder) => folder.key === key)) {
|
||||
@@ -105,11 +105,11 @@ function dummyStorage (storagePath, override = {}) {
|
||||
|
||||
sander.writeFileSync(path.join(storagePath, 'boostnote.json'), JSON.stringify(jsonData))
|
||||
var notesData = []
|
||||
var noteCount = Math.floor((Math.random() * 15)) + 1
|
||||
var noteCount = Math.floor((Math.random() * 15)) + 2
|
||||
for (var i = 0; i < noteCount; i++) {
|
||||
var key = keygen()
|
||||
var key = keygen(true)
|
||||
while (notesData.some((note) => note.key === key)) {
|
||||
key = keygen()
|
||||
key = keygen(true)
|
||||
}
|
||||
|
||||
var noteData = dummyNote({
|
||||
@@ -149,9 +149,9 @@ function dummyLegacyStorage (storagePath, override = {}) {
|
||||
var folderNotes = []
|
||||
var noteCount = Math.floor((Math.random() * 5)) + 1
|
||||
for (var i = 0; i < noteCount; i++) {
|
||||
var key = keygen(6)
|
||||
var key = keygen(true)
|
||||
while (folderNotes.some((note) => note.key === key)) {
|
||||
key = keygen(6)
|
||||
key = keygen(true)
|
||||
}
|
||||
|
||||
var noteData = dummyNote({
|
||||
|
||||
@@ -38,11 +38,6 @@ var config = Object.assign({}, skeleton, {
|
||||
'NODE_ENV': JSON.stringify('production'),
|
||||
'BABEL_ENV': JSON.stringify('production')
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compressor: {
|
||||
warnings: false
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
143
yarn.lock
143
yarn.lock
@@ -114,6 +114,12 @@ ansi-styles@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
ansi-styles@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
|
||||
@@ -1381,6 +1387,14 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
|
||||
strip-ansi "^3.0.0"
|
||||
supports-color "^2.0.0"
|
||||
|
||||
chalk@^2.3.0, chalk@^2.3.1:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
|
||||
dependencies:
|
||||
ansi-styles "^3.2.1"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
charenc@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||
@@ -1523,6 +1537,12 @@ color-convert@^1.3.0:
|
||||
dependencies:
|
||||
color-name "^1.1.1"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
|
||||
dependencies:
|
||||
color-name "^1.1.1"
|
||||
|
||||
color-name@^1.0.0, color-name@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.2.tgz#5c8ab72b64bd2215d617ae9559ebb148475cf98d"
|
||||
@@ -2057,6 +2077,13 @@ doctrine@^2.0.0:
|
||||
esutils "^2.0.2"
|
||||
isarray "^1.0.0"
|
||||
|
||||
dom-serializer@0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
|
||||
dependencies:
|
||||
domelementtype "~1.1.1"
|
||||
entities "~1.1.1"
|
||||
|
||||
dom-storage@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.0.2.tgz#ed17cbf68abd10e0aef8182713e297c5e4b500b0"
|
||||
@@ -2069,6 +2096,27 @@ domain-browser@^1.1.1:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
|
||||
|
||||
domelementtype@1, domelementtype@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
|
||||
|
||||
domelementtype@~1.1.1:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
|
||||
|
||||
domhandler@^2.3.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
|
||||
dependencies:
|
||||
domelementtype "1"
|
||||
|
||||
domutils@^1.5.1:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
|
||||
dependencies:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
dot-prop@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177"
|
||||
@@ -2249,7 +2297,7 @@ enhanced-resolve@~0.9.0:
|
||||
memory-fs "^0.2.0"
|
||||
tapable "^0.1.8"
|
||||
|
||||
entities@~1.1.1:
|
||||
entities@^1.1.1, entities@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
|
||||
|
||||
@@ -3274,6 +3322,10 @@ has-flag@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
|
||||
has-unicode@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
|
||||
@@ -3361,6 +3413,17 @@ html-encoding-sniffer@^1.0.1:
|
||||
dependencies:
|
||||
whatwg-encoding "^1.0.1"
|
||||
|
||||
htmlparser2@^3.9.0:
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
|
||||
dependencies:
|
||||
domelementtype "^1.3.0"
|
||||
domhandler "^2.3.0"
|
||||
domutils "^1.5.1"
|
||||
entities "^1.1.1"
|
||||
inherits "^2.0.1"
|
||||
readable-stream "^2.0.2"
|
||||
|
||||
http-errors@~1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257"
|
||||
@@ -3414,6 +3477,10 @@ iconv-lite@0.4.13, iconv-lite@~0.4.13:
|
||||
version "0.4.13"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
|
||||
|
||||
iconv-lite@^0.4.19:
|
||||
version "0.4.19"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
|
||||
|
||||
iconv-lite@~0.2.11:
|
||||
version "0.2.11"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8"
|
||||
@@ -3906,11 +3973,11 @@ jsx-ast-utils@^2.0.0:
|
||||
dependencies:
|
||||
array-includes "^3.0.3"
|
||||
|
||||
katex@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/katex/-/katex-0.8.3.tgz#909d99864baf964c3ccae39c4a99a8e0fb9a1bd0"
|
||||
katex@^0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/katex/-/katex-0.9.0.tgz#26a7d082c21d53725422d2d71da9b2d8455fbd4a"
|
||||
dependencies:
|
||||
match-at "^0.1.0"
|
||||
match-at "^0.1.1"
|
||||
|
||||
kind-of@^3.0.2:
|
||||
version "3.2.2"
|
||||
@@ -4015,14 +4082,30 @@ lodash.difference@^4.3.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
|
||||
|
||||
lodash.escaperegexp@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
|
||||
|
||||
lodash.get@^4.0.0:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
|
||||
lodash.isplainobject@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
|
||||
lodash.isstring@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
|
||||
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
|
||||
lodash.mergewith@^4.6.0:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
|
||||
|
||||
lodash.some@^4.5.1:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
|
||||
@@ -4158,9 +4241,9 @@ markdown-it@^6.0.1:
|
||||
mdurl "~1.0.1"
|
||||
uc.micro "^1.0.1"
|
||||
|
||||
match-at@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/match-at/-/match-at-0.1.0.tgz#f561e7709ff9a105b85cc62c6b8ee7c15bf24f31"
|
||||
match-at@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/match-at/-/match-at-0.1.1.tgz#25d040d291777704d5e6556bbb79230ec2de0540"
|
||||
|
||||
matcher@^0.1.1:
|
||||
version "0.1.2"
|
||||
@@ -5138,6 +5221,14 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
|
||||
source-map "^0.5.6"
|
||||
supports-color "^3.2.3"
|
||||
|
||||
postcss@^6.0.14:
|
||||
version "6.0.19"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.19.tgz#76a78386f670b9d9494a655bf23ac012effd1555"
|
||||
dependencies:
|
||||
chalk "^2.3.1"
|
||||
source-map "^0.6.1"
|
||||
supports-color "^5.2.0"
|
||||
|
||||
power-assert-context-formatter@^1.0.4:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/power-assert-context-formatter/-/power-assert-context-formatter-1.1.1.tgz#edba352d3ed8a603114d667265acce60d689ccdf"
|
||||
@@ -5843,6 +5934,21 @@ sander@^0.5.1:
|
||||
mkdirp "^0.5.1"
|
||||
rimraf "^2.5.2"
|
||||
|
||||
sanitize-html@^1.18.2:
|
||||
version "1.18.2"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.18.2.tgz#61877ba5a910327e42880a28803c2fbafa8e4642"
|
||||
dependencies:
|
||||
chalk "^2.3.0"
|
||||
htmlparser2 "^3.9.0"
|
||||
lodash.clonedeep "^4.5.0"
|
||||
lodash.escaperegexp "^4.1.2"
|
||||
lodash.isplainobject "^4.0.6"
|
||||
lodash.isstring "^4.0.1"
|
||||
lodash.mergewith "^4.6.0"
|
||||
postcss "^6.0.14"
|
||||
srcset "^1.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
sax@0.5.x:
|
||||
version "0.5.8"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1"
|
||||
@@ -6008,6 +6114,10 @@ source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
||||
|
||||
source-map@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
|
||||
source-map@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"
|
||||
@@ -6046,6 +6156,13 @@ sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
|
||||
srcset@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef"
|
||||
dependencies:
|
||||
array-uniq "^1.0.2"
|
||||
number-is-nan "^1.0.0"
|
||||
|
||||
sshpk@^1.7.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.0.tgz#ff2a3e4fd04497555fed97b39a0fd82fafb3a33c"
|
||||
@@ -6283,6 +6400,12 @@ supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.2.3:
|
||||
dependencies:
|
||||
has-flag "^1.0.0"
|
||||
|
||||
supports-color@^5.2.0, supports-color@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0"
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
svgo@^0.7.0:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
|
||||
@@ -6672,6 +6795,10 @@ uuid@^2.0.1, uuid@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
|
||||
|
||||
uuid@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
|
||||
|
||||
Reference in New Issue
Block a user