mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
merge from master
This commit is contained in:
@@ -14,18 +14,20 @@ import {
|
||||
import TextEditorInterface from 'browser/lib/TextEditorInterface'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import iconv from 'iconv-lite'
|
||||
import crypto from 'crypto'
|
||||
import consts from 'browser/lib/consts'
|
||||
|
||||
import { isMarkdownTitleURL } from 'browser/lib/utils'
|
||||
import styles from '../components/CodeEditor.styl'
|
||||
import fs from 'fs'
|
||||
const { ipcRenderer, remote, clipboard } = require('electron')
|
||||
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||
const spellcheck = require('browser/lib/spellcheck')
|
||||
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
|
||||
import TurndownService from 'turndown'
|
||||
import {
|
||||
gfm
|
||||
} from 'turndown-plugin-gfm'
|
||||
import {languageMaps} from '../lib/CMLanguageList'
|
||||
import snippetManager from '../lib/SnippetManager'
|
||||
import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator'
|
||||
import markdownlint from 'markdownlint'
|
||||
import Jsonlint from 'jsonlint-mod'
|
||||
import { DEFAULT_CONFIG } from '../main/lib/ConfigManager'
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
@@ -38,85 +40,6 @@ function translateHotkey (hotkey) {
|
||||
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
|
||||
}
|
||||
|
||||
const languageMaps = {
|
||||
brainfuck: 'Brainfuck',
|
||||
cpp: 'C++',
|
||||
cs: 'C#',
|
||||
clojure: 'Clojure',
|
||||
'clojure-repl': 'ClojureScript',
|
||||
cmake: 'CMake',
|
||||
coffeescript: 'CoffeeScript',
|
||||
crystal: 'Crystal',
|
||||
css: 'CSS',
|
||||
d: 'D',
|
||||
dart: 'Dart',
|
||||
delphi: 'Pascal',
|
||||
diff: 'Diff',
|
||||
django: 'Django',
|
||||
dockerfile: 'Dockerfile',
|
||||
ebnf: 'EBNF',
|
||||
elm: 'Elm',
|
||||
erlang: 'Erlang',
|
||||
'erlang-repl': 'Erlang',
|
||||
fortran: 'Fortran',
|
||||
fsharp: 'F#',
|
||||
gherkin: 'Gherkin',
|
||||
go: 'Go',
|
||||
groovy: 'Groovy',
|
||||
haml: 'HAML',
|
||||
haskell: 'Haskell',
|
||||
haxe: 'Haxe',
|
||||
http: 'HTTP',
|
||||
ini: 'toml',
|
||||
java: 'Java',
|
||||
javascript: 'JavaScript',
|
||||
json: 'JSON',
|
||||
julia: 'Julia',
|
||||
kotlin: 'Kotlin',
|
||||
less: 'LESS',
|
||||
livescript: 'LiveScript',
|
||||
lua: 'Lua',
|
||||
markdown: 'Markdown',
|
||||
mathematica: 'Mathematica',
|
||||
nginx: 'Nginx',
|
||||
nsis: 'NSIS',
|
||||
objectivec: 'Objective-C',
|
||||
ocaml: 'Ocaml',
|
||||
perl: 'Perl',
|
||||
php: 'PHP',
|
||||
powershell: 'PowerShell',
|
||||
properties: 'Properties files',
|
||||
protobuf: 'ProtoBuf',
|
||||
python: 'Python',
|
||||
puppet: 'Puppet',
|
||||
q: 'Q',
|
||||
r: 'R',
|
||||
ruby: 'Ruby',
|
||||
rust: 'Rust',
|
||||
sas: 'SAS',
|
||||
scala: 'Scala',
|
||||
scheme: 'Scheme',
|
||||
scss: 'SCSS',
|
||||
shell: 'Shell',
|
||||
smalltalk: 'Smalltalk',
|
||||
sml: 'SML',
|
||||
sql: 'SQL',
|
||||
stylus: 'Stylus',
|
||||
swift: 'Swift',
|
||||
tcl: 'Tcl',
|
||||
tex: 'LaTex',
|
||||
typescript: 'TypeScript',
|
||||
twig: 'Twig',
|
||||
vbnet: 'VB.NET',
|
||||
vbscript: 'VBScript',
|
||||
verilog: 'Verilog',
|
||||
vhdl: 'VHDL',
|
||||
xml: 'HTML',
|
||||
xquery: 'XQuery',
|
||||
yaml: 'YAML',
|
||||
elixir: 'Elixir'
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -163,6 +86,8 @@ export default class CodeEditor extends React.Component {
|
||||
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||
this.searchState = null
|
||||
this.scrollToLineHandeler = this.scrollToLine.bind(this)
|
||||
this.getCodeEditorLintConfig = this.getCodeEditorLintConfig.bind(this)
|
||||
this.validatorOfMarkdown = this.validatorOfMarkdown.bind(this)
|
||||
|
||||
this.formatTable = () => this.handleFormatTable()
|
||||
|
||||
@@ -228,7 +153,8 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
updateDefaultKeyMap () {
|
||||
const { hotkey } = this.props
|
||||
const expandSnippet = this.expandSnippet.bind(this)
|
||||
const self = this
|
||||
const expandSnippet = snippetManager.expandSnippet
|
||||
|
||||
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
|
||||
Tab: function (cm) {
|
||||
@@ -248,14 +174,16 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
cm.execCommand('goLineEnd')
|
||||
} else if (
|
||||
!charBeforeCursor.match(/\t|\s|\r|\n/) &&
|
||||
!charBeforeCursor.match(/\t|\s|\r|\n|\$/) &&
|
||||
cursor.ch > 1
|
||||
) {
|
||||
// text expansion on tab key if the char before is alphabet
|
||||
const snippets = JSON.parse(
|
||||
fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
|
||||
const wordBeforeCursor = self.getWordBeforeCursor(
|
||||
line,
|
||||
cursor.line,
|
||||
cursor.ch
|
||||
)
|
||||
if (expandSnippet(line, cursor, cm, snippets) === false) {
|
||||
if (expandSnippet(wordBeforeCursor, cursor, cm) === false) {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
@@ -277,6 +205,26 @@ export default class CodeEditor extends React.Component {
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
'Ctrl-/': function (cm) {
|
||||
if (global.process.platform === 'darwin') { return }
|
||||
const dateNow = new Date()
|
||||
cm.replaceSelection(dateNow.toLocaleDateString())
|
||||
},
|
||||
'Cmd-/': function (cm) {
|
||||
if (global.process.platform !== 'darwin') { return }
|
||||
const dateNow = new Date()
|
||||
cm.replaceSelection(dateNow.toLocaleDateString())
|
||||
},
|
||||
'Shift-Ctrl-/': function (cm) {
|
||||
if (global.process.platform === 'darwin') { return }
|
||||
const dateNow = new Date()
|
||||
cm.replaceSelection(dateNow.toLocaleString())
|
||||
},
|
||||
'Shift-Cmd-/': function (cm) {
|
||||
if (global.process.platform !== 'darwin') { return }
|
||||
const dateNow = new Date()
|
||||
cm.replaceSelection(dateNow.toLocaleString())
|
||||
},
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': cm => {
|
||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
@@ -307,25 +255,10 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { rulers, enableRulers } = this.props
|
||||
const { rulers, enableRulers, enableMarkdownLint } = this.props
|
||||
eventEmitter.on('line:jump', this.scrollToLineHandeler)
|
||||
|
||||
const defaultSnippet = [
|
||||
{
|
||||
id: crypto.randomBytes(16).toString('hex'),
|
||||
name: 'Dummy text',
|
||||
prefix: ['lorem', 'ipsum'],
|
||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||
}
|
||||
]
|
||||
if (!fs.existsSync(consts.SNIPPET_FILE)) {
|
||||
fs.writeFileSync(
|
||||
consts.SNIPPET_FILE,
|
||||
JSON.stringify(defaultSnippet, null, 4),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
snippetManager.init()
|
||||
this.updateDefaultKeyMap()
|
||||
|
||||
this.value = this.props.value
|
||||
@@ -344,7 +277,8 @@ export default class CodeEditor extends React.Component {
|
||||
inputStyle: 'textarea',
|
||||
dragDrop: false,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
autoCloseBrackets: {
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
@@ -354,6 +288,8 @@ export default class CodeEditor extends React.Component {
|
||||
extraKeys: this.defaultKeyMap
|
||||
})
|
||||
|
||||
document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none'
|
||||
|
||||
if (!this.props.mode && this.props.value && this.props.autoDetect) {
|
||||
this.autoDetectLanguage(this.props.value)
|
||||
} else {
|
||||
@@ -520,61 +456,12 @@ export default class CodeEditor extends React.Component {
|
||||
this.initialHighlighting()
|
||||
}
|
||||
|
||||
expandSnippet (line, cursor, cm, snippets) {
|
||||
const wordBeforeCursor = this.getWordBeforeCursor(
|
||||
line,
|
||||
cursor.line,
|
||||
cursor.ch
|
||||
)
|
||||
const templateCursorString = ':{}'
|
||||
for (let i = 0; i < snippets.length; i++) {
|
||||
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
|
||||
if (snippets[i].content.indexOf(templateCursorString) !== -1) {
|
||||
const snippetLines = snippets[i].content.split('\n')
|
||||
let cursorLineNumber = 0
|
||||
let cursorLinePosition = 0
|
||||
|
||||
let cursorIndex
|
||||
for (let j = 0; j < snippetLines.length; j++) {
|
||||
cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||
|
||||
if (cursorIndex !== -1) {
|
||||
cursorLineNumber = j
|
||||
cursorLinePosition = cursorIndex
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cm.replaceRange(
|
||||
snippets[i].content.replace(templateCursorString, ''),
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
cm.setCursor({
|
||||
line: cursor.line + cursorLineNumber,
|
||||
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
|
||||
})
|
||||
} else {
|
||||
cm.replaceRange(
|
||||
snippets[i].content,
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
getWordBeforeCursor (line, lineNumber, cursorPosition) {
|
||||
let wordBeforeCursor = ''
|
||||
const originCursorPosition = cursorPosition
|
||||
const emptyChars = /\t|\s|\r|\n/
|
||||
const emptyChars = /\t|\s|\r|\n|\$/
|
||||
|
||||
// to prevent the word to expand is long that will crash the whole app
|
||||
// to prevent the word is long that will crash the whole app
|
||||
// the safeStop is there to stop user to expand words that longer than 20 chars
|
||||
const safeStop = 20
|
||||
|
||||
@@ -584,7 +471,7 @@ export default class CodeEditor extends React.Component {
|
||||
if (!emptyChars.test(currentChar)) {
|
||||
wordBeforeCursor = currentChar + wordBeforeCursor
|
||||
} else if (wordBeforeCursor.length >= safeStop) {
|
||||
throw new Error('Your snippet trigger is too long !')
|
||||
throw new Error('Stopped after 20 loops for safety reason !')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -629,7 +516,9 @@ export default class CodeEditor extends React.Component {
|
||||
let needRefresh = false
|
||||
const {
|
||||
rulers,
|
||||
enableRulers
|
||||
enableRulers,
|
||||
enableMarkdownLint,
|
||||
customMarkdownLintConfig
|
||||
} = this.props
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.setMode(this.props.mode)
|
||||
@@ -647,6 +536,16 @@ export default class CodeEditor extends React.Component {
|
||||
if (prevProps.keyMap !== this.props.keyMap) {
|
||||
needRefresh = true
|
||||
}
|
||||
if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) {
|
||||
if (!enableMarkdownLint) {
|
||||
this.editor.setOption('lint', {default: false})
|
||||
document.querySelector('.CodeMirror-lint-markers').style.display = 'none'
|
||||
} else {
|
||||
this.editor.setOption('lint', this.getCodeEditorLintConfig())
|
||||
document.querySelector('.CodeMirror-lint-markers').style.display = 'inline-block'
|
||||
}
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.enableRulers !== enableRulers ||
|
||||
@@ -727,6 +626,56 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getCodeEditorLintConfig () {
|
||||
const { mode } = this.props
|
||||
const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown'
|
||||
|
||||
return checkMarkdownNoteIsOpen ? {
|
||||
'getAnnotations': this.validatorOfMarkdown,
|
||||
'async': true
|
||||
} : false
|
||||
}
|
||||
|
||||
validatorOfMarkdown (text, updateLinting) {
|
||||
const { customMarkdownLintConfig } = this.props
|
||||
let lintConfigJson
|
||||
try {
|
||||
Jsonlint.parse(customMarkdownLintConfig)
|
||||
lintConfigJson = JSON.parse(customMarkdownLintConfig)
|
||||
} catch (err) {
|
||||
eventEmitter.emit('APP_SETTING_ERROR')
|
||||
return
|
||||
}
|
||||
const lintOptions = {
|
||||
'strings': {
|
||||
'content': text
|
||||
},
|
||||
'config': lintConfigJson
|
||||
}
|
||||
|
||||
return markdownlint(lintOptions, (err, result) => {
|
||||
if (!err) {
|
||||
const foundIssues = []
|
||||
const splitText = text.split('\n')
|
||||
result.content.map(item => {
|
||||
let ruleNames = ''
|
||||
item.ruleNames.map((ruleName, index) => {
|
||||
ruleNames += ruleName
|
||||
ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/'
|
||||
})
|
||||
const lineNumber = item.lineNumber - 1
|
||||
foundIssues.push({
|
||||
from: CodeMirror.Pos(lineNumber, 0),
|
||||
to: CodeMirror.Pos(lineNumber, splitText[lineNumber].length),
|
||||
message: ruleNames + item.ruleDescription,
|
||||
severity: 'warning'
|
||||
})
|
||||
})
|
||||
updateLinting(foundIssues)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setMode (mode) {
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text'))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
@@ -738,6 +687,34 @@ export default class CodeEditor extends React.Component {
|
||||
handleChange (editor, changeObject) {
|
||||
spellcheck.handleChange(editor, changeObject)
|
||||
|
||||
// The current note contains an toc. We'll check for changes on headlines.
|
||||
// origin is undefined when markdownTocGenerator replace the old tod
|
||||
if (tocExistsInEditor(editor) && changeObject.origin !== undefined) {
|
||||
let requireTocUpdate
|
||||
|
||||
// Check if one of the changed lines contains a headline
|
||||
for (let line = 0; line < changeObject.text.length; line++) {
|
||||
if (this.linePossibleContainsHeadline(editor.getLine(changeObject.from.line + line))) {
|
||||
requireTocUpdate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!requireTocUpdate) {
|
||||
// Check if one of the removed lines contains a headline
|
||||
for (let line = 0; line < changeObject.removed.length; line++) {
|
||||
if (this.linePossibleContainsHeadline(changeObject.removed[line])) {
|
||||
requireTocUpdate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requireTocUpdate) {
|
||||
generateInEditor(editor)
|
||||
}
|
||||
}
|
||||
|
||||
this.updateHighlight(editor, changeObject)
|
||||
|
||||
this.value = editor.getValue()
|
||||
@@ -746,6 +723,12 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
linePossibleContainsHeadline (currentLine) {
|
||||
// We can't check if the line start with # because when some write text before
|
||||
// the # we also need to update the toc
|
||||
return currentLine.includes('# ')
|
||||
}
|
||||
|
||||
incrementLines (start, linesAdded, linesRemoved, editor) {
|
||||
const highlightedLines = editor.options.linesHighlighted
|
||||
|
||||
@@ -835,6 +818,9 @@ export default class CodeEditor extends React.Component {
|
||||
ch: 1
|
||||
}
|
||||
this.editor.setCursor(cursor)
|
||||
const top = this.editor.charCoords({line: num, ch: 0}, 'local').top
|
||||
const middleHeight = this.editor.getScrollerElement().offsetHeight / 2
|
||||
this.editor.scrollTo(null, top - middleHeight - 5)
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -950,6 +936,8 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
if (isInFencedCodeBlock(editor)) {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
} else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(editor, pastedTxt)
|
||||
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(editor, pastedTxt)
|
||||
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||
@@ -991,7 +979,17 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
handlePasteUrl (editor, pastedTxt) {
|
||||
const taggedUrl = `<${pastedTxt}>`
|
||||
let taggedUrl = `<${pastedTxt}>`
|
||||
let urlToFetch = pastedTxt
|
||||
let titleMark = ''
|
||||
|
||||
if (isMarkdownTitleURL(pastedTxt)) {
|
||||
const pastedTxtSplitted = pastedTxt.split(' ')
|
||||
titleMark = `${pastedTxtSplitted[0]} `
|
||||
urlToFetch = pastedTxtSplitted[1]
|
||||
taggedUrl = `<${urlToFetch}>`
|
||||
}
|
||||
|
||||
editor.replaceSelection(taggedUrl)
|
||||
|
||||
const isImageReponse = response => {
|
||||
@@ -1003,22 +1001,23 @@ export default class CodeEditor extends React.Component {
|
||||
const replaceTaggedUrl = replacement => {
|
||||
const value = editor.getValue()
|
||||
const cursor = editor.getCursor()
|
||||
const newValue = value.replace(taggedUrl, replacement)
|
||||
const newValue = value.replace(taggedUrl, titleMark + replacement)
|
||||
const newCursor = Object.assign({}, cursor, {
|
||||
ch: cursor.ch + newValue.length - value.length
|
||||
ch: cursor.ch + newValue.length - (value.length - titleMark.length)
|
||||
})
|
||||
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(newCursor)
|
||||
}
|
||||
|
||||
fetch(pastedTxt, {
|
||||
fetch(urlToFetch, {
|
||||
method: 'get'
|
||||
})
|
||||
.then(response => {
|
||||
if (isImageReponse(response)) {
|
||||
return this.mapImageResponse(response, pastedTxt)
|
||||
return this.mapImageResponse(response, urlToFetch)
|
||||
} else {
|
||||
return this.mapNormalResponse(response, pastedTxt)
|
||||
return this.mapNormalResponse(response, urlToFetch)
|
||||
}
|
||||
})
|
||||
.then(replacement => {
|
||||
@@ -1140,8 +1139,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={
|
||||
{
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize: fontSize,
|
||||
width: width,
|
||||
@@ -1185,7 +1183,9 @@ CodeEditor.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
autoDetect: PropTypes.bool,
|
||||
spellCheck: PropTypes.bool
|
||||
spellCheck: PropTypes.bool,
|
||||
enableMarkdownLint: PropTypes.bool,
|
||||
customMarkdownLintConfig: PropTypes.string
|
||||
}
|
||||
|
||||
CodeEditor.defaultProps = {
|
||||
@@ -1197,5 +1197,7 @@ CodeEditor.defaultProps = {
|
||||
indentSize: 4,
|
||||
indentType: 'space',
|
||||
autoDetect: false,
|
||||
spellCheck: false
|
||||
spellCheck: false,
|
||||
enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint,
|
||||
customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig
|
||||
}
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
|
||||
.spellcheck-select
|
||||
border: none
|
||||
text-decoration underline wavy red
|
||||
|
||||
@@ -32,6 +32,7 @@ class MarkdownEditor extends React.Component {
|
||||
componentDidMount () {
|
||||
this.value = this.refs.code.value
|
||||
eventEmitter.on('editor:lock', this.lockEditorCode)
|
||||
eventEmitter.on('editor:focus', this.focusEditor.bind(this))
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
@@ -47,6 +48,15 @@ class MarkdownEditor extends React.Component {
|
||||
componentWillUnmount () {
|
||||
this.cancelQueue()
|
||||
eventEmitter.off('editor:lock', this.lockEditorCode)
|
||||
eventEmitter.off('editor:focus', this.focusEditor.bind(this))
|
||||
}
|
||||
|
||||
focusEditor () {
|
||||
this.setState({
|
||||
status: 'CODE'
|
||||
}, () => {
|
||||
this.refs.code.focus()
|
||||
})
|
||||
}
|
||||
|
||||
queueRendering (value) {
|
||||
@@ -149,10 +159,10 @@ class MarkdownEditor extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
const checkReplace = /\[x\]/i
|
||||
const uncheckReplace = /\[ \]/
|
||||
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
|
||||
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
|
||||
const checkReplace = /\[x]/i
|
||||
const uncheckReplace = /\[ ]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
@@ -309,6 +319,8 @@ class MarkdownEditor extends React.Component {
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
switchPreview={config.editor.switchPreview}
|
||||
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||
/>
|
||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||
? 'preview'
|
||||
|
||||
@@ -8,7 +8,7 @@ import consts from 'browser/lib/consts'
|
||||
import Raphael from 'raphael'
|
||||
import flowchart from 'flowchart'
|
||||
import mermaidRender from './render/MermaidRender'
|
||||
import SequenceDiagram from 'js-sequence-diagrams'
|
||||
import SequenceDiagram from '@rokt33r/js-sequence-diagrams'
|
||||
import Chart from 'chart.js'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||
@@ -192,6 +192,19 @@ const defaultCodeBlockFontFamily = [
|
||||
'source-code-pro',
|
||||
'monospace'
|
||||
]
|
||||
|
||||
// return the line number of the line that used to generate the specified element
|
||||
// return -1 if the line is not found
|
||||
function getSourceLineNumberByElement (element) {
|
||||
let isHasLineNumber = element.dataset.line !== undefined
|
||||
let parent = element
|
||||
while (!isHasLineNumber && parent.parentElement !== null) {
|
||||
parent = parent.parentElement
|
||||
isHasLineNumber = parent.dataset.line !== undefined
|
||||
}
|
||||
return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1
|
||||
}
|
||||
|
||||
export default class MarkdownPreview extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -208,6 +221,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||
this.saveAsPdfHandler = () => this.handleSaveAsPdf()
|
||||
this.printHandler = () => this.handlePrint()
|
||||
|
||||
this.linkClickHandler = this.handleLinkClick.bind(this)
|
||||
@@ -241,7 +255,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
return
|
||||
}
|
||||
// No contextMenu was passed to us -> execute our own link-opener
|
||||
if (event.target.tagName.toLowerCase() === 'a') {
|
||||
if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) {
|
||||
const href = event.target.href
|
||||
const isLocalFile = href.startsWith('file:')
|
||||
if (isLocalFile) {
|
||||
@@ -268,17 +282,27 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
handleMouseDown (e) {
|
||||
const config = ConfigManager.get()
|
||||
const clickElement = e.target
|
||||
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV"
|
||||
const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
|
||||
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
||||
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
||||
}
|
||||
if (e.target != null) {
|
||||
switch (e.target.tagName) {
|
||||
case 'A':
|
||||
case 'INPUT':
|
||||
return null
|
||||
if (e.ctrlKey) {
|
||||
if (config.editor.type === 'SPLIT') {
|
||||
if (lineNumber !== -1) {
|
||||
eventEmitter.emit('line:jump', lineNumber)
|
||||
}
|
||||
} else {
|
||||
if (lineNumber !== -1) {
|
||||
eventEmitter.emit('editor:focus')
|
||||
eventEmitter.emit('line:jump', lineNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
|
||||
|
||||
if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e)
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
@@ -297,58 +321,77 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.exportAsDocument('md')
|
||||
}
|
||||
|
||||
handleSaveAsHtml () {
|
||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.getStyleParams()
|
||||
htmlContentFormatter (noteContent, exportTasks, targetDir) {
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
)
|
||||
const body = this.markdown.render(noteContent)
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
files.forEach(file => {
|
||||
if (global.process.platform === 'win32') {
|
||||
file = file.replace('file:///', '')
|
||||
} else {
|
||||
file = file.replace('file://', '')
|
||||
}
|
||||
exportTasks.push({
|
||||
src: file,
|
||||
dst: 'css'
|
||||
const inlineStyles = buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
)
|
||||
const body = this.markdown.render(noteContent)
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
files.forEach(file => {
|
||||
if (global.process.platform === 'win32') {
|
||||
file = file.replace('file:///', '')
|
||||
} else {
|
||||
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>
|
||||
<base href="file://${targetDir}/">
|
||||
<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>`
|
||||
}
|
||||
|
||||
handleSaveAsHtml () {
|
||||
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir)))
|
||||
}
|
||||
|
||||
handleSaveAsPdf () {
|
||||
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
|
||||
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false}})
|
||||
printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir))
|
||||
return new Promise((resolve, reject) => {
|
||||
printout.webContents.on('did-finish-load', () => {
|
||||
printout.webContents.printToPDF({}, (err, data) => {
|
||||
if (err) reject(err)
|
||||
else resolve(data)
|
||||
printout.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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>`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -490,6 +533,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||
eventEmitter.on('export:save-pdf', this.saveAsPdfHandler)
|
||||
eventEmitter.on('print', this.printHandler)
|
||||
}
|
||||
|
||||
@@ -527,6 +571,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||
eventEmitter.off('export:save-pdf', this.saveAsPdfHandler)
|
||||
eventEmitter.off('print', this.printHandler)
|
||||
}
|
||||
|
||||
@@ -625,14 +670,14 @@ export default class MarkdownPreview extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
GetCodeThemeLink (theme) {
|
||||
theme = consts.THEMES.some(_theme => _theme === theme) &&
|
||||
theme !== 'default'
|
||||
? theme
|
||||
: 'elegant'
|
||||
return theme.startsWith('solarized')
|
||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
||||
GetCodeThemeLink (name) {
|
||||
const theme = consts.THEMES.find(theme => theme.name === name)
|
||||
|
||||
if (theme) {
|
||||
return `${appPath}/${theme.path}`
|
||||
} else {
|
||||
return `${appPath}/node_modules/codemirror/theme/elegant.css`
|
||||
}
|
||||
}
|
||||
|
||||
rewriteIframe () {
|
||||
@@ -690,9 +735,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
)
|
||||
|
||||
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
|
||||
? codeBlockTheme
|
||||
: 'default'
|
||||
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
|
||||
|
||||
const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default'
|
||||
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
||||
@@ -705,6 +750,8 @@ export default class MarkdownPreview extends React.Component {
|
||||
copyIcon.innerHTML =
|
||||
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||
copyIcon.onclick = e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
copy(content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
@@ -713,14 +760,11 @@ export default class MarkdownPreview extends React.Component {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
el.parentNode.appendChild(copyIcon)
|
||||
el.innerHTML = ''
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||
} else {
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||
}
|
||||
el.parentNode.className += ` ${codeBlockThemeClassName}`
|
||||
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: indentSize
|
||||
})
|
||||
@@ -835,78 +879,96 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
|
||||
const rect = markdownPreviewIframe.getBoundingClientRect()
|
||||
const config = { attributes: true, subtree: true }
|
||||
const imgObserver = new MutationObserver((mutationList) => {
|
||||
for (const mu of mutationList) {
|
||||
if (mu.target.className === 'carouselContent-enter-done') {
|
||||
this.setImgOnClickEventHelper(mu.target, rect)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
|
||||
for (const img of imgList) {
|
||||
img.onclick = () => {
|
||||
const widthMagnification = document.body.clientWidth / img.width
|
||||
const heightMagnification = document.body.clientHeight / img.height
|
||||
const baseOnWidth = widthMagnification < heightMagnification
|
||||
const magnification = baseOnWidth ? widthMagnification : heightMagnification
|
||||
const parentEl = img.parentElement
|
||||
this.setImgOnClickEventHelper(img, rect)
|
||||
imgObserver.observe(parentEl, config)
|
||||
}
|
||||
}
|
||||
|
||||
const zoomImgWidth = img.width * magnification
|
||||
const zoomImgHeight = img.height * magnification
|
||||
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
|
||||
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
|
||||
const originalImgTop = img.y + rect.top
|
||||
const originalImgLeft = img.x + rect.left
|
||||
const originalImgRect = {
|
||||
top: `${originalImgTop}px`,
|
||||
left: `${originalImgLeft}px`,
|
||||
width: `${img.width}px`,
|
||||
height: `${img.height}px`
|
||||
}
|
||||
const zoomInImgRect = {
|
||||
top: `${baseOnWidth ? zoomImgTop : 0}px`,
|
||||
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
|
||||
width: `${zoomImgWidth}px`,
|
||||
height: `${zoomImgHeight}px`
|
||||
}
|
||||
const animationSpeed = 300
|
||||
setImgOnClickEventHelper (img, rect) {
|
||||
img.onclick = () => {
|
||||
const widthMagnification = document.body.clientWidth / img.width
|
||||
const heightMagnification = document.body.clientHeight / img.height
|
||||
const baseOnWidth = widthMagnification < heightMagnification
|
||||
const magnification = baseOnWidth ? widthMagnification : heightMagnification
|
||||
|
||||
const zoomImg = document.createElement('img')
|
||||
zoomImg.src = img.src
|
||||
const zoomImgWidth = img.width * magnification
|
||||
const zoomImgHeight = img.height * magnification
|
||||
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
|
||||
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
|
||||
const originalImgTop = img.y + rect.top
|
||||
const originalImgLeft = img.x + rect.left
|
||||
const originalImgRect = {
|
||||
top: `${originalImgTop}px`,
|
||||
left: `${originalImgLeft}px`,
|
||||
width: `${img.width}px`,
|
||||
height: `${img.height}px`
|
||||
}
|
||||
const zoomInImgRect = {
|
||||
top: `${baseOnWidth ? zoomImgTop : 0}px`,
|
||||
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
|
||||
width: `${zoomImgWidth}px`,
|
||||
height: `${zoomImgHeight}px`
|
||||
}
|
||||
const animationSpeed = 300
|
||||
|
||||
const zoomImg = document.createElement('img')
|
||||
zoomImg.src = img.src
|
||||
zoomImg.style = `
|
||||
position: absolute;
|
||||
top: ${baseOnWidth ? zoomImgTop : 0}px;
|
||||
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
|
||||
width: ${zoomImgWidth};
|
||||
height: ${zoomImgHeight}px;
|
||||
`
|
||||
zoomImg.animate([
|
||||
originalImgRect,
|
||||
zoomInImgRect
|
||||
], animationSpeed)
|
||||
|
||||
const overlay = document.createElement('div')
|
||||
overlay.style = `
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
cursor: zoom-out;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: ${document.body.clientHeight}px;
|
||||
z-index: 100;
|
||||
`
|
||||
overlay.onclick = () => {
|
||||
zoomImg.style = `
|
||||
position: absolute;
|
||||
top: ${baseOnWidth ? zoomImgTop : 0}px;
|
||||
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
|
||||
width: ${zoomImgWidth};
|
||||
height: ${zoomImgHeight}px;
|
||||
top: ${originalImgTop}px;
|
||||
left: ${originalImgLeft}px;
|
||||
width: ${img.width}px;
|
||||
height: ${img.height}px;
|
||||
`
|
||||
zoomImg.animate([
|
||||
originalImgRect,
|
||||
zoomInImgRect
|
||||
const zoomOutImgAnimation = zoomImg.animate([
|
||||
zoomInImgRect,
|
||||
originalImgRect
|
||||
], animationSpeed)
|
||||
|
||||
const overlay = document.createElement('div')
|
||||
overlay.style = `
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
cursor: zoom-out;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: ${document.body.clientHeight}px;
|
||||
z-index: 100;
|
||||
`
|
||||
overlay.onclick = () => {
|
||||
zoomImg.style = `
|
||||
position: absolute;
|
||||
top: ${originalImgTop}px;
|
||||
left: ${originalImgLeft}px;
|
||||
width: ${img.width}px;
|
||||
height: ${img.height}px;
|
||||
`
|
||||
const zoomOutImgAnimation = zoomImg.animate([
|
||||
zoomInImgRect,
|
||||
originalImgRect
|
||||
], animationSpeed)
|
||||
zoomOutImgAnimation.onfinish = () => overlay.remove()
|
||||
}
|
||||
|
||||
overlay.appendChild(zoomImg)
|
||||
document.body.appendChild(overlay)
|
||||
zoomOutImgAnimation.onfinish = () => overlay.remove()
|
||||
}
|
||||
|
||||
overlay.appendChild(zoomImg)
|
||||
document.body.appendChild(overlay)
|
||||
}
|
||||
|
||||
this.getWindow().scrollTo(0, 0)
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -953,13 +1015,19 @@ export default class MarkdownPreview extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const href = e.target.href
|
||||
const linkHash = href.split('/').pop()
|
||||
const rawHref = e.target.getAttribute('href')
|
||||
const parser = document.createElement('a')
|
||||
parser.href = e.target.getAttribute('href')
|
||||
const { href, hash } = parser
|
||||
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
|
||||
|
||||
const regexNoteInternalLink = /main.html#(.+)/
|
||||
if (regexNoteInternalLink.test(linkHash)) {
|
||||
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
|
||||
const targetElement = this.refs.root.contentWindow.document.getElementById(
|
||||
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
|
||||
|
||||
const regexNoteInternalLink = /.*[main.\w]*.html#/
|
||||
|
||||
if (regexNoteInternalLink.test(href)) {
|
||||
const targetId = mdurl.encode(linkHash)
|
||||
const targetElement = this.refs.root.contentWindow.document.querySelector(
|
||||
targetId
|
||||
)
|
||||
|
||||
|
||||
@@ -79,10 +79,10 @@ class MarkdownSplitEditor extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
const checkReplace = /\[x\]/i
|
||||
const uncheckReplace = /\[ \]/
|
||||
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
|
||||
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
|
||||
const checkReplace = /\[x]/i
|
||||
const uncheckReplace = /\[ ]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
@@ -224,6 +224,8 @@ class MarkdownSplitEditor extends React.Component {
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
switchPreview={config.editor.switchPreview}
|
||||
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||
/>
|
||||
<div styleName={isStacking ? 'slider-hoz' : 'slider'} style={{left: sliderStyle.left, top: sliderStyle.top}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||
<div styleName='slider-hitbox' />
|
||||
|
||||
@@ -16,8 +16,8 @@ const NavToggleButton = ({isFolded, handleToggleButtonClick}) => (
|
||||
onClick={(e) => handleToggleButtonClick(e)}
|
||||
>
|
||||
{isFolded
|
||||
? <i className='fa fa-angle-double-right' />
|
||||
: <i className='fa fa-angle-double-left' />
|
||||
? <i className='fa fa-angle-double-right fa-2x' />
|
||||
: <i className='fa fa-angle-double-left fa-2x' />
|
||||
}
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
border-radius 16.5px
|
||||
height 34px
|
||||
width 34px
|
||||
line-height 32px
|
||||
line-height 100%
|
||||
padding 0
|
||||
&:hover
|
||||
border: 1px solid #1EC38B;
|
||||
|
||||
78
browser/lib/CMLanguageList.js
Normal file
78
browser/lib/CMLanguageList.js
Normal file
@@ -0,0 +1,78 @@
|
||||
export const languageMaps = {
|
||||
brainfuck: 'Brainfuck',
|
||||
cpp: 'C++',
|
||||
cs: 'C#',
|
||||
clojure: 'Clojure',
|
||||
'clojure-repl': 'ClojureScript',
|
||||
cmake: 'CMake',
|
||||
coffeescript: 'CoffeeScript',
|
||||
crystal: 'Crystal',
|
||||
css: 'CSS',
|
||||
d: 'D',
|
||||
dart: 'Dart',
|
||||
delphi: 'Pascal',
|
||||
diff: 'Diff',
|
||||
django: 'Django',
|
||||
dockerfile: 'Dockerfile',
|
||||
ebnf: 'EBNF',
|
||||
elm: 'Elm',
|
||||
erlang: 'Erlang',
|
||||
'erlang-repl': 'Erlang',
|
||||
fortran: 'Fortran',
|
||||
fsharp: 'F#',
|
||||
gherkin: 'Gherkin',
|
||||
go: 'Go',
|
||||
groovy: 'Groovy',
|
||||
haml: 'HAML',
|
||||
haskell: 'Haskell',
|
||||
haxe: 'Haxe',
|
||||
http: 'HTTP',
|
||||
ini: 'toml',
|
||||
java: 'Java',
|
||||
javascript: 'JavaScript',
|
||||
json: 'JSON',
|
||||
julia: 'Julia',
|
||||
kotlin: 'Kotlin',
|
||||
less: 'LESS',
|
||||
livescript: 'LiveScript',
|
||||
lua: 'Lua',
|
||||
markdown: 'Markdown',
|
||||
mathematica: 'Mathematica',
|
||||
nginx: 'Nginx',
|
||||
nsis: 'NSIS',
|
||||
objectivec: 'Objective-C',
|
||||
ocaml: 'Ocaml',
|
||||
perl: 'Perl',
|
||||
php: 'PHP',
|
||||
powershell: 'PowerShell',
|
||||
properties: 'Properties files',
|
||||
protobuf: 'ProtoBuf',
|
||||
python: 'Python',
|
||||
puppet: 'Puppet',
|
||||
q: 'Q',
|
||||
r: 'R',
|
||||
ruby: 'Ruby',
|
||||
rust: 'Rust',
|
||||
sas: 'SAS',
|
||||
scala: 'Scala',
|
||||
scheme: 'Scheme',
|
||||
scss: 'SCSS',
|
||||
shell: 'Shell',
|
||||
smalltalk: 'Smalltalk',
|
||||
sml: 'SML',
|
||||
sql: 'SQL',
|
||||
stylus: 'Stylus',
|
||||
swift: 'Swift',
|
||||
tcl: 'Tcl',
|
||||
tex: 'LaTex',
|
||||
typescript: 'TypeScript',
|
||||
twig: 'Twig',
|
||||
vbnet: 'VB.NET',
|
||||
vbscript: 'VBScript',
|
||||
verilog: 'Verilog',
|
||||
vhdl: 'VHDL',
|
||||
xml: 'HTML',
|
||||
xquery: 'XQuery',
|
||||
yaml: 'YAML',
|
||||
elixir: 'Elixir'
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import CSSModules from 'react-css-modules'
|
||||
|
||||
export default function (component, styles) {
|
||||
return CSSModules(component, styles, {errorWhenNotFound: false})
|
||||
return CSSModules(component, styles, {handleNotFoundStyleName: 'log'})
|
||||
}
|
||||
|
||||
@@ -62,10 +62,12 @@ const languages = [
|
||||
{
|
||||
name: 'Spanish',
|
||||
locale: 'es-ES'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: 'Turkish',
|
||||
locale: 'tr'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: 'Thai',
|
||||
locale: 'th'
|
||||
}
|
||||
@@ -82,4 +84,3 @@ module.exports = {
|
||||
return languages
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
91
browser/lib/SnippetManager.js
Normal file
91
browser/lib/SnippetManager.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
import consts from './consts'
|
||||
|
||||
class SnippetManager {
|
||||
constructor () {
|
||||
this.defaultSnippet = [
|
||||
{
|
||||
id: crypto.randomBytes(16).toString('hex'),
|
||||
name: 'Dummy text',
|
||||
prefix: ['lorem', 'ipsum'],
|
||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||
}
|
||||
]
|
||||
this.snippets = []
|
||||
this.expandSnippet = this.expandSnippet.bind(this)
|
||||
this.init = this.init.bind(this)
|
||||
this.assignSnippets = this.assignSnippets.bind(this)
|
||||
}
|
||||
|
||||
init () {
|
||||
if (fs.existsSync(consts.SNIPPET_FILE)) {
|
||||
try {
|
||||
this.snippets = JSON.parse(
|
||||
fs.readFileSync(consts.SNIPPET_FILE, { encoding: 'UTF-8' })
|
||||
)
|
||||
} catch (error) {
|
||||
console.log('Error while parsing snippet file')
|
||||
}
|
||||
return
|
||||
}
|
||||
fs.writeFileSync(
|
||||
consts.SNIPPET_FILE,
|
||||
JSON.stringify(this.defaultSnippet, null, 4),
|
||||
'utf8'
|
||||
)
|
||||
this.snippets = this.defaultSnippet
|
||||
}
|
||||
|
||||
assignSnippets (snippets) {
|
||||
this.snippets = snippets
|
||||
}
|
||||
|
||||
expandSnippet (wordBeforeCursor, cursor, cm) {
|
||||
const templateCursorString = ':{}'
|
||||
for (let i = 0; i < this.snippets.length; i++) {
|
||||
if (this.snippets[i].prefix.indexOf(wordBeforeCursor.text) === -1) {
|
||||
continue
|
||||
}
|
||||
if (this.snippets[i].content.indexOf(templateCursorString) !== -1) {
|
||||
const snippetLines = this.snippets[i].content.split('\n')
|
||||
let cursorLineNumber = 0
|
||||
let cursorLinePosition = 0
|
||||
|
||||
let cursorIndex
|
||||
for (let j = 0; j < snippetLines.length; j++) {
|
||||
cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||
|
||||
if (cursorIndex !== -1) {
|
||||
cursorLineNumber = j
|
||||
cursorLinePosition = cursorIndex
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cm.replaceRange(
|
||||
this.snippets[i].content.replace(templateCursorString, ''),
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
cm.setCursor({
|
||||
line: cursor.line + cursorLineNumber,
|
||||
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
|
||||
})
|
||||
} else {
|
||||
cm.replaceRange(
|
||||
this.snippets[i].content,
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const manager = new SnippetManager()
|
||||
export default manager
|
||||
@@ -3,14 +3,43 @@ const fs = require('sander')
|
||||
const { remote } = require('electron')
|
||||
const { app } = remote
|
||||
|
||||
const themePath = process.env.NODE_ENV === 'production'
|
||||
? path.join(app.getAppPath(), './node_modules/codemirror/theme')
|
||||
: require('path').resolve('./node_modules/codemirror/theme')
|
||||
const themes = fs.readdirSync(themePath)
|
||||
.map((themePath) => {
|
||||
return themePath.substring(0, themePath.lastIndexOf('.'))
|
||||
})
|
||||
themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
|
||||
const CODEMIRROR_THEME_PATH = 'node_modules/codemirror/theme'
|
||||
const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme'
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const paths = [
|
||||
isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH),
|
||||
isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH)
|
||||
]
|
||||
|
||||
const themes = paths
|
||||
.map(directory => fs.readdirSync(directory).map(file => {
|
||||
const name = file.substring(0, file.lastIndexOf('.'))
|
||||
|
||||
return {
|
||||
name,
|
||||
path: path.join(directory.split(/\//g).slice(-3).join('/'), file),
|
||||
className: `cm-s-${name}`
|
||||
}
|
||||
}))
|
||||
.reduce((accumulator, value) => accumulator.concat(value), [])
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, {
|
||||
name: 'solarized dark',
|
||||
path: `${CODEMIRROR_THEME_PATH}/solarized.css`,
|
||||
className: `cm-s-solarized cm-s-dark`
|
||||
}, {
|
||||
name: 'solarized light',
|
||||
path: `${CODEMIRROR_THEME_PATH}/solarized.css`,
|
||||
className: `cm-s-solarized cm-s-light`
|
||||
})
|
||||
|
||||
themes.splice(0, 0, {
|
||||
name: 'default',
|
||||
path: `${CODEMIRROR_THEME_PATH}/elegant.css`,
|
||||
className: `cm-s-default`
|
||||
})
|
||||
|
||||
const snippetFile = process.env.NODE_ENV !== 'test'
|
||||
? path.join(app.getPath('userData'), 'snippets.json')
|
||||
@@ -35,7 +64,7 @@ const consts = {
|
||||
'Dodger Blue',
|
||||
'Violet Eggplant'
|
||||
],
|
||||
THEMES: ['default'].concat(themes),
|
||||
THEMES: themes,
|
||||
SNIPPET_FILE: snippetFile,
|
||||
DEFAULT_EDITOR_FONT_FAMILY: [
|
||||
'Monaco',
|
||||
|
||||
@@ -4,11 +4,11 @@ export function getTodoStatus (content) {
|
||||
let numberOfCompletedTodo = 0
|
||||
|
||||
splitted.forEach((line) => {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
|
||||
const trimmedLine = line.trim().replace(/^(>\s*)*/, '')
|
||||
if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) {
|
||||
numberOfTodo++
|
||||
}
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
|
||||
if (trimmedLine.match(/^[+\-*] \[x] ./i)) {
|
||||
numberOfCompletedTodo++
|
||||
}
|
||||
})
|
||||
|
||||
@@ -28,6 +28,8 @@ function linkify (token) {
|
||||
const TOC_MARKER_START = '<!-- toc -->'
|
||||
const TOC_MARKER_END = '<!-- tocstop -->'
|
||||
|
||||
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
|
||||
|
||||
/**
|
||||
* Takes care of proper updating given editor with TOC.
|
||||
* If TOC doesn't exit in the editor, it's inserted at current caret position.
|
||||
@@ -35,12 +37,6 @@ const TOC_MARKER_END = '<!-- tocstop -->'
|
||||
* @param editor CodeMirror editor to be updated with TOC
|
||||
*/
|
||||
export function generateInEditor (editor) {
|
||||
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
|
||||
|
||||
function tocExistsInEditor () {
|
||||
return tocRegex.test(editor.getValue())
|
||||
}
|
||||
|
||||
function updateExistingToc () {
|
||||
const toc = generate(editor.getValue())
|
||||
const search = editor.getSearchCursor(tocRegex)
|
||||
@@ -54,13 +50,17 @@ export function generateInEditor (editor) {
|
||||
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
||||
}
|
||||
|
||||
if (tocExistsInEditor()) {
|
||||
if (tocExistsInEditor(editor)) {
|
||||
updateExistingToc()
|
||||
} else {
|
||||
addTocAtCursorPosition()
|
||||
}
|
||||
}
|
||||
|
||||
export function tocExistsInEditor (editor) {
|
||||
return tocRegex.test(editor.getValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates MD TOC based on MD document passed as string.
|
||||
* @param markdownText MD document
|
||||
@@ -94,5 +94,6 @@ function wrapTocWithEol (toc, editor) {
|
||||
|
||||
export default {
|
||||
generate,
|
||||
generateInEditor
|
||||
generateInEditor,
|
||||
tocExistsInEditor
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ class Markdown {
|
||||
|
||||
const updatedOptions = Object.assign(defaultOptions, options)
|
||||
this.md = markdownit(updatedOptions)
|
||||
this.md.linkify.set({ fuzzyLink: false })
|
||||
|
||||
if (updatedOptions.sanitize !== 'NONE') {
|
||||
const allowedTags = ['iframe', 'input', 'b',
|
||||
@@ -181,7 +182,7 @@ class Markdown {
|
||||
})
|
||||
|
||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||
this.md.use(require('markdown-it-plantuml'), '', {
|
||||
this.md.use(require('markdown-it-plantuml'), {
|
||||
generateSource: function (umlCode) {
|
||||
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
||||
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg'
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { hashHistory } from 'react-router'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import queryString from 'query-string'
|
||||
import { push } from 'connected-react-router'
|
||||
|
||||
export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||
@@ -28,10 +29,10 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
|
||||
note: note
|
||||
})
|
||||
|
||||
hashHistory.push({
|
||||
dispatch(push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
search: queryString.stringify({ key: noteHash })
|
||||
}))
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
})
|
||||
@@ -70,10 +71,10 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
dispatch(push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
search: queryString.stringify({ key: noteHash })
|
||||
}))
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ let self
|
||||
|
||||
function getAvailableDictionaries () {
|
||||
return [
|
||||
{label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED},
|
||||
{label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED},
|
||||
{label: i18n.__('English'), value: 'en_GB'},
|
||||
{label: i18n.__('German'), value: 'de_DE'},
|
||||
{label: i18n.__('French'), value: 'fr_FR'}
|
||||
|
||||
@@ -132,8 +132,13 @@ export function isObjectEqual (a, b) {
|
||||
return true
|
||||
}
|
||||
|
||||
export function isMarkdownTitleURL (str) {
|
||||
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
|
||||
}
|
||||
|
||||
export default {
|
||||
lastFindInArray,
|
||||
escapeHtmlCharacters,
|
||||
isObjectEqual
|
||||
isObjectEqual,
|
||||
isMarkdownTitleURL
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class InfoPanel extends React.Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
|
||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf, wordCount, letterCount, type, print
|
||||
} = this.props
|
||||
return (
|
||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||
@@ -85,6 +85,11 @@ class InfoPanel extends React.Component {
|
||||
<p>{i18n.__('.html')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
|
||||
<i className='fa fa-file-pdf-o' />
|
||||
<p>{i18n.__('.pdf')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
|
||||
<i className='fa fa-print' />
|
||||
<p>{i18n.__('Print')}</p>
|
||||
@@ -104,6 +109,7 @@ InfoPanel.propTypes = {
|
||||
exportAsMd: PropTypes.func.isRequired,
|
||||
exportAsTxt: PropTypes.func.isRequired,
|
||||
exportAsHtml: PropTypes.func.isRequired,
|
||||
exportAsPdf: PropTypes.func.isRequired,
|
||||
wordCount: PropTypes.number,
|
||||
letterCount: PropTypes.number,
|
||||
type: PropTypes.string.isRequired,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
right 25px
|
||||
position absolute
|
||||
padding 20px 25px 0 25px
|
||||
width 300px
|
||||
// width 300px
|
||||
overflow auto
|
||||
background-color $ui-noteList-backgroundColor
|
||||
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
||||
|
||||
@@ -5,7 +5,7 @@ import styles from './InfoPanel.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const InfoPanelTrashed = ({
|
||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
|
||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf
|
||||
}) => (
|
||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
||||
<div>
|
||||
@@ -46,7 +46,7 @@ const InfoPanelTrashed = ({
|
||||
<p>.html</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--unable'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
|
||||
<i className='fa fa-file-pdf-o' />
|
||||
<p>.pdf</p>
|
||||
</button>
|
||||
@@ -61,7 +61,8 @@ InfoPanelTrashed.propTypes = {
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
exportAsMd: PropTypes.func.isRequired,
|
||||
exportAsTxt: PropTypes.func.isRequired,
|
||||
exportAsHtml: PropTypes.func.isRequired
|
||||
exportAsHtml: PropTypes.func.isRequired,
|
||||
exportAsPdf: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(InfoPanelTrashed, styles)
|
||||
|
||||
@@ -9,7 +9,6 @@ import StarButton from './StarButton'
|
||||
import TagSelect from './TagSelect'
|
||||
import FolderSelect from './FolderSelect'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import { hashHistory } from 'react-router'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import markdown from 'browser/lib/markdownTextHelper'
|
||||
import StatusBar from '../StatusBar'
|
||||
@@ -31,6 +30,8 @@ import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||
import striptags from 'striptags'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||
import queryString from 'query-string'
|
||||
import { replace } from 'connected-react-router'
|
||||
|
||||
class MarkdownNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -142,6 +143,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
handleFolderChange (e) {
|
||||
const { dispatch } = this.props
|
||||
const { note } = this.state
|
||||
const value = this.refs.folder.value
|
||||
const splitted = value.split('-')
|
||||
@@ -161,12 +163,12 @@ class MarkdownNoteDetail extends React.Component {
|
||||
originNote: note,
|
||||
note: newNote
|
||||
})
|
||||
hashHistory.replace({
|
||||
dispatch(replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
search: queryString.stringify({
|
||||
key: newNote.key
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
this.setState({
|
||||
isMovingNote: false
|
||||
})
|
||||
@@ -203,6 +205,10 @@ class MarkdownNoteDetail extends React.Component {
|
||||
ee.emit('export:save-html')
|
||||
}
|
||||
|
||||
exportAsPdf () {
|
||||
ee.emit('export:save-pdf')
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// tab key
|
||||
@@ -438,6 +444,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
exportAsHtml={this.exportAsHtml}
|
||||
exportAsMd={this.exportAsMd}
|
||||
exportAsTxt={this.exportAsTxt}
|
||||
exportAsPdf={this.exportAsPdf}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -503,12 +510,13 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.exportAsMd}
|
||||
exportAsTxt={this.exportAsTxt}
|
||||
exportAsHtml={this.exportAsHtml}
|
||||
exportAsPdf={this.exportAsPdf}
|
||||
wordCount={note.content.split(' ').length}
|
||||
letterCount={note.content.replace(/\r?\n/g, '').length}
|
||||
type={note.type}
|
||||
|
||||
@@ -80,4 +80,12 @@ body[data-theme="monokai"]
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
div
|
||||
> button, div
|
||||
-webkit-user-drag none
|
||||
user-select none
|
||||
> img, span
|
||||
-webkit-user-drag none
|
||||
user-select none
|
||||
|
||||
@@ -8,7 +8,6 @@ import StarButton from './StarButton'
|
||||
import TagSelect from './TagSelect'
|
||||
import FolderSelect from './FolderSelect'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import {hashHistory} from 'react-router'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
@@ -18,7 +17,6 @@ import context from 'browser/lib/context'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import _ from 'lodash'
|
||||
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import FullscreenButton from './FullscreenButton'
|
||||
import TrashButton from './TrashButton'
|
||||
@@ -31,6 +29,8 @@ import { formatDate } from 'browser/lib/date-formatter'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||
import queryString from 'query-string'
|
||||
import { replace } from 'connected-react-router'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
@@ -166,12 +166,12 @@ class SnippetNoteDetail extends React.Component {
|
||||
originNote: note,
|
||||
note: newNote
|
||||
})
|
||||
hashHistory.replace({
|
||||
dispatch(replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
search: queryString.stringify({
|
||||
key: newNote.key
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
this.setState({
|
||||
isMovingNote: false
|
||||
})
|
||||
@@ -657,6 +657,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
'export-txt': 'Text export',
|
||||
'export-md': 'Markdown export',
|
||||
'export-html': 'HTML export',
|
||||
'export-pdf': 'PDF export',
|
||||
'print': 'Print'
|
||||
})[msg]
|
||||
|
||||
@@ -770,6 +771,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
exportAsHtml={this.showWarning}
|
||||
exportAsPdf={this.showWarning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -812,12 +814,13 @@ class SnippetNoteDetail extends React.Component {
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
exportAsHtml={this.showWarning}
|
||||
exportAsPdf={this.showWarning}
|
||||
type={note.type}
|
||||
print={this.showWarning}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import StatusBar from '../StatusBar'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import debounceRender from 'react-debounce-render'
|
||||
import searchFromNotes from 'browser/lib/search'
|
||||
import queryString from 'query-string'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
@@ -36,11 +37,11 @@ class Detail extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { location, data, params, config } = this.props
|
||||
const { location, data, match: { params }, config } = this.props
|
||||
const noteKey = location.search !== '' && queryString.parse(location.search).key
|
||||
let note = null
|
||||
|
||||
if (location.query.key != null) {
|
||||
const noteKey = location.query.key
|
||||
if (location.search !== '') {
|
||||
const allNotes = data.noteMap.map(note => note)
|
||||
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||
let displayedNotes = allNotes
|
||||
|
||||
16
browser/main/DevTools/index.dev.js
Normal file
16
browser/main/DevTools/index.dev.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import { createDevTools } from 'redux-devtools'
|
||||
import LogMonitor from 'redux-devtools-log-monitor'
|
||||
import DockMonitor from 'redux-devtools-dock-monitor'
|
||||
|
||||
const DevTools = createDevTools(
|
||||
<DockMonitor
|
||||
toggleVisibilityKey='ctrl-h'
|
||||
changePositionKey='ctrl-q'
|
||||
defaultIsVisible={false}
|
||||
>
|
||||
<LogMonitor theme='tomorrow' />
|
||||
</DockMonitor>
|
||||
)
|
||||
|
||||
export default DevTools
|
||||
8
browser/main/DevTools/index.js
Normal file
8
browser/main/DevTools/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/* eslint-disable no-undef */
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// eslint-disable-next-line global-require
|
||||
module.exports = require('./index.prod').default
|
||||
} else {
|
||||
// eslint-disable-next-line global-require
|
||||
module.exports = require('./index.dev').default
|
||||
}
|
||||
6
browser/main/DevTools/index.prod.js
Normal file
6
browser/main/DevTools/index.prod.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
const DevTools = () => <div />
|
||||
DevTools.instrument = () => {}
|
||||
|
||||
export default DevTools
|
||||
@@ -12,11 +12,11 @@ import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import { hashHistory } from 'react-router'
|
||||
import store from 'browser/main/store'
|
||||
import { store } from 'browser/main/store'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { getLocales } from 'browser/lib/Languages'
|
||||
import applyShortcuts from 'browser/main/lib/shortcutManager'
|
||||
import { push } from 'connected-react-router'
|
||||
const path = require('path')
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
@@ -132,7 +132,7 @@ class Main extends React.Component {
|
||||
.then(() => data.storage)
|
||||
})
|
||||
.then(storage => {
|
||||
hashHistory.push('/storages/' + storage.key)
|
||||
store.dispatch(push('/storages/' + storage.key))
|
||||
})
|
||||
.catch(err => {
|
||||
throw err
|
||||
@@ -311,7 +311,7 @@ class Main extends React.Component {
|
||||
onMouseUp={e => this.handleMouseUp(e)}
|
||||
>
|
||||
<SideNav
|
||||
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
|
||||
{..._.pick(this.props, ['dispatch', 'data', 'config', 'match', 'location'])}
|
||||
width={this.state.navWidth}
|
||||
/>
|
||||
{!config.isSideNavFolded &&
|
||||
@@ -341,7 +341,7 @@ class Main extends React.Component {
|
||||
'dispatch',
|
||||
'config',
|
||||
'data',
|
||||
'params',
|
||||
'match',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
@@ -351,7 +351,7 @@ class Main extends React.Component {
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'match',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
@@ -373,7 +373,7 @@ class Main extends React.Component {
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'match',
|
||||
'location'
|
||||
])}
|
||||
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
||||
|
||||
@@ -21,23 +21,20 @@ class NewNoteButton extends React.Component {
|
||||
this.state = {
|
||||
}
|
||||
|
||||
this.newNoteHandler = () => {
|
||||
this.handleNewNoteButtonClick()
|
||||
}
|
||||
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
eventEmitter.on('top:new-note', this.newNoteHandler)
|
||||
eventEmitter.on('top:new-note', this.handleNewNoteButtonClick)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
eventEmitter.off('top:new-note', this.newNoteHandler)
|
||||
eventEmitter.off('top:new-note', this.handleNewNoteButtonClick)
|
||||
}
|
||||
|
||||
handleNewNoteButtonClick (e) {
|
||||
const { location, params, dispatch, config } = this.props
|
||||
const { location, dispatch, match: { params }, config } = this.props
|
||||
const { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
||||
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||
@@ -55,9 +52,8 @@ class NewNoteButton extends React.Component {
|
||||
}
|
||||
|
||||
resolveTargetFolder () {
|
||||
const { data, params } = this.props
|
||||
const { data, match: { params } } = this.props
|
||||
let storage = data.storageMap.get(params.storageKey)
|
||||
|
||||
// Find first storage
|
||||
if (storage == null) {
|
||||
for (const kv of data.storageMap) {
|
||||
@@ -93,7 +89,7 @@ class NewNoteButton extends React.Component {
|
||||
>
|
||||
<div styleName='control'>
|
||||
<button styleName='control-newNoteButton'
|
||||
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
||||
onClick={this.handleNewNoteButtonClick}>
|
||||
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
|
||||
<span styleName='control-newNoteButton-tooltip'>
|
||||
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
||||
|
||||
@@ -14,23 +14,42 @@ import NoteItemSimple from 'browser/components/NoteItemSimple'
|
||||
import searchFromNotes from 'browser/lib/search'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { hashHistory } from 'react-router'
|
||||
import { push, replace } from 'connected-react-router'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import Markdown from '../../lib/markdown'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import context from 'browser/lib/context'
|
||||
import queryString from 'query-string'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { dialog } = remote
|
||||
const WP_POST_PATH = '/wp/v2/posts'
|
||||
|
||||
const regexMatchStartingTitleNumber = new RegExp('^([0-9]*\.?[0-9]+).*$')
|
||||
|
||||
function sortByCreatedAt (a, b) {
|
||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||
}
|
||||
|
||||
function sortByAlphabetical (a, b) {
|
||||
const matchA = regexMatchStartingTitleNumber.exec(a.title)
|
||||
const matchB = regexMatchStartingTitleNumber.exec(b.title)
|
||||
|
||||
if (matchA && matchA.length === 2 && matchB && matchB.length === 2) {
|
||||
// Both note titles are starting with a float. We will compare it now.
|
||||
const floatA = parseFloat(matchA[1])
|
||||
const floatB = parseFloat(matchB[1])
|
||||
|
||||
const diff = floatA - floatB
|
||||
if (diff !== 0) {
|
||||
return diff
|
||||
}
|
||||
|
||||
// The float values are equal. We will compare the full title.
|
||||
}
|
||||
|
||||
return a.title.localeCompare(b.title)
|
||||
}
|
||||
|
||||
@@ -127,15 +146,15 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { location } = this.props
|
||||
const { dispatch, 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 visibleNoteKeys = this.notes && this.notes.map(note => note.key)
|
||||
const note = this.notes && this.notes[0]
|
||||
const key = location.search && queryString.parse(location.search).key
|
||||
const prevKey = prevProps.location.search && queryString.parse(prevProps.location.search).key
|
||||
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
||||
|
||||
if (note && location.query.key == null) {
|
||||
const { router } = this.context
|
||||
if (note && location.search === '') {
|
||||
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
||||
|
||||
// A visible note is an active note
|
||||
@@ -145,17 +164,17 @@ class NoteList extends React.Component {
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
|
||||
router.replace({
|
||||
dispatch(replace({ // was passed with context - we can use connected router here
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
search: queryString.stringify({
|
||||
key: noteKey
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// Auto scroll
|
||||
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
|
||||
if (_.isString(key) && prevKey === key) {
|
||||
const targetIndex = this.getTargetIndex()
|
||||
if (targetIndex > -1) {
|
||||
const list = this.refs.list
|
||||
@@ -176,18 +195,18 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
focusNote (selectedNoteKeys, noteKey, pathname) {
|
||||
const { router } = this.context
|
||||
const { dispatch } = this.props
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys
|
||||
})
|
||||
|
||||
router.push({
|
||||
dispatch(push({
|
||||
pathname,
|
||||
query: {
|
||||
search: queryString.stringify({
|
||||
key: noteKey
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
getNoteKeyFromTargetIndex (targetIndex) {
|
||||
@@ -259,13 +278,22 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
jumpNoteByHashHandler (event, noteHash) {
|
||||
const { data } = this.props
|
||||
|
||||
// first argument event isn't used.
|
||||
if (this.notes === null || this.notes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedNoteKeys = [noteHash]
|
||||
this.focusNote(selectedNoteKeys, noteHash, '/home')
|
||||
|
||||
let locationToSelect = '/home'
|
||||
const noteByHash = data.noteMap.map((note) => note).find(note => note.key === noteHash)
|
||||
if (noteByHash !== undefined) {
|
||||
locationToSelect = '/storages/' + noteByHash.storage + '/folders/' + noteByHash.folder
|
||||
}
|
||||
|
||||
this.focusNote(selectedNoteKeys, noteHash, locationToSelect)
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
@@ -321,8 +349,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
getNotes () {
|
||||
const { data, params, location } = this.props
|
||||
|
||||
const { data, match: { params }, location } = this.props
|
||||
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
|
||||
const allNotes = data.noteMap.map((note) => note)
|
||||
this.contextNotes = allNotes
|
||||
@@ -363,7 +390,7 @@ class NoteList extends React.Component {
|
||||
|
||||
// get notes in the current folder
|
||||
getContextNotes () {
|
||||
const { data, params } = this.props
|
||||
const { data, match: { params } } = this.props
|
||||
const storageKey = params.storageKey
|
||||
const folderKey = params.folderKey
|
||||
const storage = data.storageMap.get(storageKey)
|
||||
@@ -403,8 +430,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
const { dispatch, location } = this.props
|
||||
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
||||
const { ctrlKeyDown, shiftKeyDown } = this.state
|
||||
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||
@@ -455,16 +481,16 @@ class NoteList extends React.Component {
|
||||
prevShiftNoteIndex
|
||||
})
|
||||
|
||||
router.push({
|
||||
dispatch(push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
search: queryString.stringify({
|
||||
key: uniqueKey
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
handleSortByChange (e) {
|
||||
const { dispatch, params: { folderKey } } = this.props
|
||||
const { dispatch, match: { params: { folderKey } } } = this.props
|
||||
|
||||
const config = {
|
||||
[folderKey]: { sortBy: e.target.value }
|
||||
@@ -496,6 +522,7 @@ class NoteList extends React.Component {
|
||||
'export-txt': 'Text export',
|
||||
'export-md': 'Markdown export',
|
||||
'export-html': 'HTML export',
|
||||
'export-pdf': 'PDF export',
|
||||
'print': 'Print'
|
||||
})[msg]
|
||||
|
||||
@@ -716,7 +743,11 @@ class NoteList extends React.Component {
|
||||
folder: folder.key,
|
||||
title: firstNote.title + ' ' + i18n.__('copy'),
|
||||
content: firstNote.content,
|
||||
linesHighlighted: firstNote.linesHighlighted
|
||||
linesHighlighted: firstNote.linesHighlighted,
|
||||
description: firstNote.description,
|
||||
snippets: firstNote.snippets,
|
||||
tags: firstNote.tags,
|
||||
isStarred: firstNote.isStarred
|
||||
})
|
||||
.then((note) => {
|
||||
attachmentManagement.cloneAttachments(firstNote, note)
|
||||
@@ -732,10 +763,10 @@ class NoteList extends React.Component {
|
||||
selectedNoteKeys: [note.key]
|
||||
})
|
||||
|
||||
hashHistory.push({
|
||||
dispatch(push({
|
||||
pathname: location.pathname,
|
||||
query: {key: note.key}
|
||||
})
|
||||
search: queryString.stringify({key: note.key})
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -745,13 +776,13 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
navigate (sender, pathname) {
|
||||
const { router } = this.context
|
||||
router.push({
|
||||
const { dispatch } = this.props
|
||||
dispatch(push({
|
||||
pathname,
|
||||
query: {
|
||||
search: queryString.stringify({
|
||||
// key: noteKey
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
save (note) {
|
||||
@@ -881,7 +912,7 @@ class NoteList extends React.Component {
|
||||
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
|
||||
}
|
||||
|
||||
// Add notes to the current folder
|
||||
// Add notes to the current folder
|
||||
addNotesFromFiles (filepaths) {
|
||||
const { dispatch, location } = this.props
|
||||
const { storage, folder } = this.resolveTargetFolder()
|
||||
@@ -905,13 +936,20 @@ class NoteList extends React.Component {
|
||||
}
|
||||
dataApi.createNote(storage.key, newNote)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: getNoteKey(note)}
|
||||
attachmentManagement.importAttachments(note.content, filepath, storage.key, note.key)
|
||||
.then((newcontent) => {
|
||||
note.content = newcontent
|
||||
|
||||
dataApi.updateNote(storage.key, note.key, note)
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
dispatch(push({
|
||||
pathname: location.pathname,
|
||||
search: queryString.stringify({key: getNoteKey(note)})
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -921,14 +959,15 @@ class NoteList extends React.Component {
|
||||
|
||||
getTargetIndex () {
|
||||
const { location } = this.props
|
||||
const key = queryString.parse(location.search).key
|
||||
const targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return getNoteKey(note) === location.query.key
|
||||
return getNoteKey(note) === key
|
||||
})
|
||||
return targetIndex
|
||||
}
|
||||
|
||||
resolveTargetFolder () {
|
||||
const { data, params } = this.props
|
||||
const { data, match: { params } } = this.props
|
||||
let storage = data.storageMap.get(params.storageKey)
|
||||
|
||||
// Find first storage
|
||||
@@ -976,7 +1015,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { location, config, params: { folderKey } } = this.props
|
||||
const { location, config, match: { params: { folderKey } } } = this.props
|
||||
let { notes } = this.props
|
||||
const { selectedNoteKeys } = this.state
|
||||
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StorageItem.styl'
|
||||
import { hashHistory } from 'react-router'
|
||||
import modal from 'browser/main/lib/modal'
|
||||
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
|
||||
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
||||
@@ -12,6 +11,7 @@ import _ from 'lodash'
|
||||
import { SortableElement } from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import context from 'browser/lib/context'
|
||||
import { push } from 'connected-react-router'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { dialog } = remote
|
||||
@@ -134,14 +134,14 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
|
||||
handleHeaderInfoClick (e) {
|
||||
const { storage } = this.props
|
||||
hashHistory.push('/storages/' + storage.key)
|
||||
const { storage, dispatch } = this.props
|
||||
dispatch(push('/storages/' + storage.key))
|
||||
}
|
||||
|
||||
handleFolderButtonClick (folderKey) {
|
||||
return (e) => {
|
||||
const { storage } = this.props
|
||||
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
|
||||
const { storage, dispatch } = this.props
|
||||
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { push } from 'connected-react-router'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import styles from './SideNav.styl'
|
||||
@@ -62,7 +63,7 @@ class SideNav extends React.Component {
|
||||
})
|
||||
|
||||
if (selectedButton === 0) {
|
||||
const { data, dispatch, location, params } = this.props
|
||||
const { data, dispatch, location, match: { params } } = this.props
|
||||
|
||||
const notes = data.noteMap
|
||||
.map(note => note)
|
||||
@@ -92,7 +93,7 @@ class SideNav extends React.Component {
|
||||
if (index !== -1) {
|
||||
tags.splice(index, 1)
|
||||
|
||||
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
|
||||
dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`))
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -104,13 +105,13 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
handleHomeButtonClick (e) {
|
||||
const { router } = this.context
|
||||
router.push('/home')
|
||||
const { dispatch } = this.props
|
||||
dispatch(push('/home'))
|
||||
}
|
||||
|
||||
handleStarredButtonClick (e) {
|
||||
const { router } = this.context
|
||||
router.push('/starred')
|
||||
const { dispatch } = this.props
|
||||
dispatch(push('/starred'))
|
||||
}
|
||||
|
||||
handleTagContextMenu (e, tag) {
|
||||
@@ -190,18 +191,18 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
handleTrashedButtonClick (e) {
|
||||
const { router } = this.context
|
||||
router.push('/trashed')
|
||||
const { dispatch } = this.props
|
||||
dispatch(push('/trashed'))
|
||||
}
|
||||
|
||||
handleSwitchFoldersButtonClick () {
|
||||
const { router } = this.context
|
||||
router.push('/home')
|
||||
const { dispatch } = this.props
|
||||
dispatch(push('/home'))
|
||||
}
|
||||
|
||||
handleSwitchTagsButtonClick () {
|
||||
const { router } = this.context
|
||||
router.push('/alltags')
|
||||
const { dispatch } = this.props
|
||||
dispatch(push('/alltags'))
|
||||
}
|
||||
|
||||
onSortEnd (storage) {
|
||||
@@ -348,8 +349,8 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
handleClickTagListItem (name) {
|
||||
const { router } = this.context
|
||||
router.push(`/tags/${encodeURIComponent(name)}`)
|
||||
const { dispatch } = this.props
|
||||
dispatch(push(`/tags/${encodeURIComponent(name)}`))
|
||||
}
|
||||
|
||||
handleSortTagsByChange (e) {
|
||||
@@ -367,8 +368,7 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
handleClickNarrowToTag (tag) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
const { dispatch, location } = this.props
|
||||
const listOfTags = this.getActiveTags(location.pathname)
|
||||
const indexOfTag = listOfTags.indexOf(tag)
|
||||
if (indexOfTag > -1) {
|
||||
@@ -376,7 +376,7 @@ class SideNav extends React.Component {
|
||||
} else {
|
||||
listOfTags.push(tag)
|
||||
}
|
||||
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
|
||||
dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`))
|
||||
}
|
||||
|
||||
emptyTrash (entries) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import ee from 'browser/main/lib/eventEmitter'
|
||||
import NewNoteButton from 'browser/main/NewNoteButton'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { push } from 'connected-react-router'
|
||||
import queryString from 'query-string'
|
||||
|
||||
class TopBar extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -26,6 +28,8 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
this.codeInitHandler = this.handleCodeInit.bind(this)
|
||||
this.updateKeyword = this.updateKeyword.bind(this)
|
||||
this.handleSearchClearButton = this.handleSearchClearButton.bind(this)
|
||||
|
||||
this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, {
|
||||
maxWait: 1000 / 8
|
||||
@@ -33,8 +37,8 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { params } = this.props
|
||||
const searchWord = params.searchword
|
||||
const { match: { params } } = this.props
|
||||
const searchWord = params && params.searchword
|
||||
if (searchWord !== undefined) {
|
||||
this.setState({
|
||||
search: searchWord,
|
||||
@@ -51,13 +55,13 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
handleSearchClearButton (e) {
|
||||
const { router } = this.context
|
||||
const { dispatch } = this.props
|
||||
this.setState({
|
||||
search: '',
|
||||
isSearching: false
|
||||
})
|
||||
this.refs.search.childNodes[0].blur
|
||||
router.push('/searched')
|
||||
dispatch(push('/searched'))
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
@@ -124,7 +128,8 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
updateKeyword (keyword) {
|
||||
this.context.router.push(`/searched/${encodeURIComponent(keyword)}`)
|
||||
const { dispatch } = this.props
|
||||
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
|
||||
this.setState({
|
||||
search: keyword
|
||||
})
|
||||
@@ -210,8 +215,8 @@ class TopBar extends React.Component {
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
'location',
|
||||
'match'
|
||||
])}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Provider } from 'react-redux'
|
||||
import Main from './Main'
|
||||
import store from './store'
|
||||
import React from 'react'
|
||||
import { store, history } from './store'
|
||||
import React, { Fragment } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
require('!!style!css!stylus?sourceMap!./global.styl')
|
||||
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
|
||||
import { syncHistoryWithStore } from 'react-router-redux'
|
||||
import { Route, Switch, Redirect } from 'react-router-dom'
|
||||
import { ConnectedRouter } from 'connected-react-router'
|
||||
import DevTools from './DevTools'
|
||||
|
||||
require('./lib/ipcClient')
|
||||
require('../lib/customMeta')
|
||||
import i18n from 'browser/lib/i18n'
|
||||
@@ -77,7 +79,6 @@ document.addEventListener('click', function (e) {
|
||||
})
|
||||
|
||||
const el = document.getElementById('content')
|
||||
const history = syncHistoryWithStore(hashHistory, store)
|
||||
|
||||
function notify (...args) {
|
||||
return new window.Notification(...args)
|
||||
@@ -98,29 +99,24 @@ function updateApp () {
|
||||
|
||||
ReactDOM.render((
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<Route path='/' component={Main}>
|
||||
<IndexRedirect to='/home' />
|
||||
<Route path='home' />
|
||||
<Route path='starred' />
|
||||
<Route path='searched'>
|
||||
<Route path=':searchword' />
|
||||
</Route>
|
||||
<Route path='trashed' />
|
||||
<Route path='alltags' />
|
||||
<Route path='tags'>
|
||||
<IndexRedirect to='/alltags' />
|
||||
<Route path=':tagname' />
|
||||
</Route>
|
||||
<Route path='storages'>
|
||||
<IndexRedirect to='/home' />
|
||||
<Route path=':storageKey'>
|
||||
<IndexRoute />
|
||||
<Route path='folders/:folderKey' />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Router>
|
||||
<ConnectedRouter history={history}>
|
||||
<Fragment>
|
||||
<Switch>
|
||||
<Redirect path='/' to='/home' exact />
|
||||
<Route path='/(home|alltags|starred|trashed)' component={Main} />
|
||||
<Route path='/searched' component={Main} exact />
|
||||
<Route path='/searched/:searchword' component={Main} />
|
||||
<Redirect path='/tags' to='/alltags' exact />
|
||||
<Route path='/tags/:tagname' component={Main} />
|
||||
|
||||
{/* storages */}
|
||||
<Redirect path='/storages' to='/home' exact />
|
||||
<Route path='/storages/:storageKey' component={Main} exact />
|
||||
<Route path='/storages/:storageKey/folders/:folderKey' component={Main} />
|
||||
</Switch>
|
||||
<DevTools />
|
||||
</Fragment>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
), el, function () {
|
||||
const loadingCover = document.getElementById('loadingCover')
|
||||
|
||||
@@ -11,6 +11,10 @@ const consts = require('browser/lib/consts')
|
||||
|
||||
let isInitialized = false
|
||||
|
||||
const DEFAULT_MARKDOWN_LINT_CONFIG = `{
|
||||
"default": true
|
||||
}`
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
zoom: 1,
|
||||
isSideNavFolded: false,
|
||||
@@ -47,7 +51,7 @@ export const DEFAULT_CONFIG = {
|
||||
enableRulers: false,
|
||||
rulers: [80, 120],
|
||||
displayLineNumbers: true,
|
||||
matchingPairs: '()[]{}\'\'""$$**``',
|
||||
matchingPairs: '()[]{}\'\'""$$**``~~__',
|
||||
matchingTriples: '```"""\'\'\'',
|
||||
explodingPairs: '[]{}``$$',
|
||||
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
||||
@@ -60,7 +64,9 @@ export const DEFAULT_CONFIG = {
|
||||
enableFrontMatterTitle: true,
|
||||
frontMatterTitleField: 'title',
|
||||
spellcheck: false,
|
||||
enableSmartPaste: false
|
||||
enableSmartPaste: false,
|
||||
enableMarkdownLint: false,
|
||||
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
@@ -133,16 +139,12 @@ function get () {
|
||||
document.head.appendChild(editorTheme)
|
||||
}
|
||||
|
||||
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
||||
? config.editor.theme
|
||||
: 'default'
|
||||
const theme = consts.THEMES.find(theme => theme.name === config.editor.theme)
|
||||
|
||||
if (config.editor.theme !== 'default') {
|
||||
if (config.editor.theme.startsWith('solarized')) {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
|
||||
} else {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
||||
}
|
||||
if (theme) {
|
||||
editorTheme.setAttribute('href', `../${theme.path}`)
|
||||
} else {
|
||||
config.editor.theme = 'default'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,16 +180,11 @@ function set (updates) {
|
||||
editorTheme.setAttribute('rel', 'stylesheet')
|
||||
document.head.appendChild(editorTheme)
|
||||
}
|
||||
const newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
|
||||
? newConfig.editor.theme
|
||||
: 'default'
|
||||
|
||||
if (newTheme !== 'default') {
|
||||
if (newTheme.startsWith('solarized')) {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
|
||||
} else {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
|
||||
}
|
||||
const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme)
|
||||
|
||||
if (newTheme) {
|
||||
editorTheme.setAttribute('href', `../${newTheme.path}`)
|
||||
}
|
||||
|
||||
ipcRenderer.send('config-renew', {
|
||||
|
||||
@@ -85,7 +85,7 @@ function getOrientation (file) {
|
||||
return view.getUint16(offset + (i * 12) + 8, little)
|
||||
}
|
||||
}
|
||||
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
|
||||
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker.
|
||||
break
|
||||
} else {
|
||||
offset += view.getUint16(offset, false)
|
||||
@@ -278,27 +278,40 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||
let promise
|
||||
if (dropEvent.dataTransfer.files.length > 0) {
|
||||
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
|
||||
if (file.type.startsWith('image')) {
|
||||
if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
|
||||
return copyAttachment(file.path, storageKey, noteKey).then(fileName => ({
|
||||
const filePath = file.path
|
||||
const fileType = file.type // EX) 'image/gif' or 'text/html'
|
||||
if (fileType.startsWith('image')) {
|
||||
if (fileType === 'image/gif' || fileType === 'image/svg+xml') {
|
||||
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
|
||||
fileName,
|
||||
title: path.basename(file.path),
|
||||
title: path.basename(filePath),
|
||||
isImage: true
|
||||
}))
|
||||
} else {
|
||||
return fixRotate(file)
|
||||
.then(data => copyAttachment({type: 'base64', data: data, sourceFilePath: file.path}, storageKey, noteKey)
|
||||
.then(fileName => ({
|
||||
return getOrientation(file)
|
||||
.then((orientation) => {
|
||||
if (orientation === -1) { // The image rotation is correct and does not need adjustment
|
||||
return copyAttachment(filePath, storageKey, noteKey)
|
||||
} else {
|
||||
return fixRotate(file).then(data => copyAttachment({
|
||||
type: 'base64',
|
||||
data: data,
|
||||
sourceFilePath: filePath
|
||||
}, storageKey, noteKey))
|
||||
}
|
||||
})
|
||||
.then(fileName =>
|
||||
({
|
||||
fileName,
|
||||
title: path.basename(file.path),
|
||||
title: path.basename(filePath),
|
||||
isImage: true
|
||||
}))
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return copyAttachment(file.path, storageKey, noteKey).then(fileName => ({
|
||||
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
|
||||
fileName,
|
||||
title: path.basename(file.path),
|
||||
title: path.basename(filePath),
|
||||
isImage: false
|
||||
}))
|
||||
}
|
||||
@@ -325,13 +338,18 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||
canvas.height = image.height
|
||||
context.drawImage(image, 0, 0)
|
||||
|
||||
return copyAttachment({type: 'base64', data: canvas.toDataURL(), sourceFilePath: imageURL}, storageKey, noteKey)
|
||||
return copyAttachment({
|
||||
type: 'base64',
|
||||
data: canvas.toDataURL(),
|
||||
sourceFilePath: imageURL
|
||||
}, storageKey, noteKey)
|
||||
})
|
||||
.then(fileName => ({
|
||||
fileName,
|
||||
title: imageURL,
|
||||
isImage: true
|
||||
}))])
|
||||
}))
|
||||
])
|
||||
}
|
||||
|
||||
promise.then(files => {
|
||||
@@ -449,6 +467,54 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Copies the attachments to the storage folder and returns the mardown content it should be replaced with
|
||||
* @param {String} markDownContent content in which the attachment paths should be found
|
||||
* @param {String} filepath The path of the file with attachments to import
|
||||
* @param {String} storageKey Storage key of the destination storage
|
||||
* @param {String} noteKey Key of the current note. Will be used as subfolder in :storage
|
||||
*/
|
||||
function importAttachments (markDownContent, filepath, storageKey, noteKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const nameRegex = /(!\[.*?]\()(.+?\..+?)(\))/g
|
||||
let attachPath = nameRegex.exec(markDownContent)
|
||||
const promiseArray = []
|
||||
const attachmentPaths = []
|
||||
const groupIndex = 2
|
||||
|
||||
while (attachPath) {
|
||||
let attachmentPath = attachPath[groupIndex]
|
||||
attachmentPaths.push(attachmentPath)
|
||||
attachmentPath = path.isAbsolute(attachmentPath) ? attachmentPath : path.join(path.dirname(filepath), attachmentPath)
|
||||
promiseArray.push(this.copyAttachment(attachmentPath, storageKey, noteKey))
|
||||
attachPath = nameRegex.exec(markDownContent)
|
||||
}
|
||||
|
||||
let numResolvedPromises = 0
|
||||
|
||||
if (promiseArray.length === 0) {
|
||||
resolve(markDownContent)
|
||||
}
|
||||
|
||||
for (let j = 0; j < promiseArray.length; j++) {
|
||||
promiseArray[j]
|
||||
.then((fileName) => {
|
||||
const newPath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName)
|
||||
markDownContent = markDownContent.replace(attachmentPaths[j], newPath)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('File does not exist in path: ' + attachmentPaths[j])
|
||||
})
|
||||
.finally(() => {
|
||||
numResolvedPromises++
|
||||
if (numResolvedPromises === promiseArray.length) {
|
||||
resolve(markDownContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Moves the attachments of the current note to the new location.
|
||||
* Returns a modified version of the given content so that the links to the attachments point to the new note key.
|
||||
@@ -656,6 +722,7 @@ module.exports = {
|
||||
handlePasteNativeImage,
|
||||
getAttachmentsInMarkdownContent,
|
||||
getAbsolutePathsOfAttachmentsInContent,
|
||||
importAttachments,
|
||||
removeStorageAndNoteReferences,
|
||||
deleteAttachmentFolder,
|
||||
deleteAttachmentsNotPresentInNote,
|
||||
|
||||
@@ -43,19 +43,18 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
||||
.then(function exportNotes (data) {
|
||||
const { storage, notes } = data
|
||||
|
||||
notes
|
||||
return Promise.all(notes
|
||||
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
||||
.forEach(note => {
|
||||
.map(note => {
|
||||
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
|
||||
exportNote(note.key, storage.path, note.content, notePath, null)
|
||||
return exportNote(note.key, storage.path, note.content, notePath, null)
|
||||
})
|
||||
|
||||
return {
|
||||
).then(() => ({
|
||||
storage,
|
||||
folderKey,
|
||||
fileType,
|
||||
exportDir
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -43,14 +43,17 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt
|
||||
)
|
||||
|
||||
if (outputFormatter) {
|
||||
exportedData = outputFormatter(exportedData, exportTasks)
|
||||
exportedData = outputFormatter(exportedData, exportTasks, path.dirname(targetPath))
|
||||
} else {
|
||||
exportedData = Promise.resolve(exportedData)
|
||||
}
|
||||
|
||||
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
||||
|
||||
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
|
||||
.then(() => {
|
||||
return saveToFile(exportedData, targetPath)
|
||||
.then(() => exportedData)
|
||||
.then(data => {
|
||||
return saveToFile(data, targetPath)
|
||||
}).catch((err) => {
|
||||
rollbackExport(tasks)
|
||||
throw err
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import ReactDOM from 'react-dom'
|
||||
import store from '../store'
|
||||
import { store } from '../store'
|
||||
|
||||
class ModalBase extends React.Component {
|
||||
constructor (props) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './CreateFolderModal.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import store from 'browser/main/store'
|
||||
import { store } from 'browser/main/store'
|
||||
import consts from 'browser/lib/consts'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
|
||||
@@ -4,11 +4,12 @@ import styles from './NewNoteModal.styl'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
|
||||
import queryString from 'query-string'
|
||||
|
||||
class NewNoteModal extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.lock = false
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
@@ -21,10 +22,14 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleMarkdownNoteButtonClick (e) {
|
||||
const { storage, folder, dispatch, location, params, config } = this.props
|
||||
createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
const { storage, folder, dispatch, location, config } = this.props
|
||||
const params = location.search !== '' && queryString.parse(location.search)
|
||||
if (!this.lock) {
|
||||
this.lock = true
|
||||
createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMarkdownNoteButtonKeyDown (e) {
|
||||
@@ -35,10 +40,14 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleSnippetNoteButtonClick (e) {
|
||||
const { storage, folder, dispatch, location, params, config } = this.props
|
||||
createSnippetNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
const { storage, folder, dispatch, location, config } = this.props
|
||||
const params = location.search !== '' && queryString.parse(location.search)
|
||||
if (!this.lock) {
|
||||
this.lock = true
|
||||
createSnippetNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSnippetNoteButtonKeyDown (e) {
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 { store } from 'browser/main/store'
|
||||
import PropTypes from 'prop-types'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
margin-bottom 15px
|
||||
margin-top 30px
|
||||
|
||||
.group-header--sub
|
||||
@extend .group-header
|
||||
margin-bottom 10px
|
||||
|
||||
.group-header2--sub
|
||||
@extend .group-header2
|
||||
margin-bottom 10px
|
||||
|
||||
.group-section
|
||||
margin-bottom 20px
|
||||
display flex
|
||||
@@ -148,10 +156,12 @@ body[data-theme="dark"]
|
||||
color $ui-dark-text-color
|
||||
|
||||
.group-header
|
||||
.group-header--sub
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.group-header2
|
||||
.group-header2--sub
|
||||
color $ui-dark-text-color
|
||||
|
||||
.group-section-control-input
|
||||
@@ -176,10 +186,12 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.group-header
|
||||
.group-header--sub
|
||||
color $ui-solarized-dark-text-color
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
.group-header2
|
||||
.group-header2--sub
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.group-section-control-input
|
||||
@@ -203,10 +215,12 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.group-header
|
||||
.group-header--sub
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.group-header2
|
||||
.group-header2--sub
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.group-section-control-input
|
||||
@@ -230,10 +244,12 @@ body[data-theme="dracula"]
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.group-header
|
||||
.group-header--sub
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.group-header2
|
||||
.group-header2--sub
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.group-section-control-input
|
||||
|
||||
@@ -22,18 +22,16 @@ class Crowdfunding extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
|
||||
<div styleName='group-header'>{i18n.__('Crowdfunding')}</div>
|
||||
<p>{i18n.__('Thank you for using Boostnote!')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}</p>
|
||||
<p>{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('### Sustainable Open Source Ecosystem')}</p>
|
||||
<div styleName='group-header2--sub'>{i18n.__('Sustainable Open Source Ecosystem')}</div>
|
||||
<p>{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}</p>
|
||||
<p>{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}</p>
|
||||
<p>{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('### We believe Meritocracy')}</p>
|
||||
<div styleName='group-header2--sub'>{i18n.__('We believe Meritocracy')}</div>
|
||||
<p>{i18n.__('We think developers who have skills and do great things must be rewarded properly.')}</p>
|
||||
<p>{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}</p>
|
||||
<p>{i18n.__('It sometimes looks like exploitation.')}</p>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
@import('./Tab')
|
||||
@import('./ConfigTab')
|
||||
|
||||
.root
|
||||
padding 15px
|
||||
white-space pre
|
||||
line-height 1.4
|
||||
color alpha($ui-text-color, 90%)
|
||||
width 100%
|
||||
font-size 14px
|
||||
p
|
||||
font-size 16px
|
||||
line-height 1.4
|
||||
|
||||
.cf-link
|
||||
height 35px
|
||||
|
||||
@@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
import ReactDOM from 'react-dom'
|
||||
import styles from './FolderItem.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import store from 'browser/main/store'
|
||||
import { store } from 'browser/main/store'
|
||||
import { SketchPicker } from 'react-color'
|
||||
import { SortableElement, SortableHandle } from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import styles from './FolderList.styl'
|
||||
import store from 'browser/main/store'
|
||||
import { store } from 'browser/main/store'
|
||||
import FolderItem from './FolderItem'
|
||||
import { SortableContainer } from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { store } from 'browser/main/store'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoTab.styl'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import store from 'browser/main/store'
|
||||
import { store } from 'browser/main/store'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
@@ -69,8 +69,7 @@ class InfoTab extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div styleName='root'>
|
||||
|
||||
<div styleName='header--sub'>{i18n.__('Community')}</div>
|
||||
<div styleName='group-header'>{i18n.__('Community')}</div>
|
||||
<div styleName='top'>
|
||||
<ul styleName='list'>
|
||||
<li>
|
||||
@@ -108,7 +107,7 @@ class InfoTab extends React.Component {
|
||||
|
||||
<hr />
|
||||
|
||||
<div styleName='header--sub'>{i18n.__('About')}</div>
|
||||
<div styleName='group-header--sub'>{i18n.__('About')}</div>
|
||||
|
||||
<div styleName='top'>
|
||||
<div styleName='icon-space'>
|
||||
@@ -143,7 +142,7 @@ class InfoTab extends React.Component {
|
||||
|
||||
<hr styleName='separate-line' />
|
||||
|
||||
<div styleName='policy'>{i18n.__('Analytics')}</div>
|
||||
<div styleName='group-header2--sub'>{i18n.__('Analytics')}</div>
|
||||
<div>{i18n.__('Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.')}</div>
|
||||
<div>{i18n.__('You can see how it works on ')}<a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
|
||||
<br />
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
@import('./Tab')
|
||||
|
||||
.root
|
||||
padding 15px
|
||||
white-space pre
|
||||
line-height 1.4
|
||||
color alpha($ui-text-color, 90%)
|
||||
width 100%
|
||||
font-size 14px
|
||||
|
||||
.top
|
||||
text-align left
|
||||
margin-bottom 20px
|
||||
@import('./ConfigTab.styl')
|
||||
|
||||
.icon-space
|
||||
margin 20px 0
|
||||
@@ -45,13 +33,21 @@
|
||||
.separate-line
|
||||
margin 40px 0
|
||||
|
||||
.policy
|
||||
width 100%
|
||||
font-size 20px
|
||||
margin-bottom 10px
|
||||
|
||||
.policy-submit
|
||||
margin-top 10px
|
||||
height 35px
|
||||
border-radius 2px
|
||||
border none
|
||||
background-color alpha(#1EC38B, 90%)
|
||||
padding-left 20px
|
||||
padding-right 20px
|
||||
text-decoration none
|
||||
color white
|
||||
font-weight 600
|
||||
font-size 16px
|
||||
&:hover
|
||||
background-color #1EC38B
|
||||
transition 0.2s
|
||||
|
||||
.policy-confirm
|
||||
margin-top 10px
|
||||
@@ -60,11 +56,14 @@
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
color alpha($tab--dark-text-color, 80%)
|
||||
|
||||
.appId
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
.appId
|
||||
color $ui-solarized-dark-text-color
|
||||
.list
|
||||
a
|
||||
color $ui-solarized-dark-active-color
|
||||
@@ -72,6 +71,8 @@ body[data-theme="solarized-dark"]
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-monokai-text-color
|
||||
.appId
|
||||
color $ui-monokai-text-color
|
||||
.list
|
||||
a
|
||||
color $ui-monokai-active-color
|
||||
@@ -79,6 +80,8 @@ body[data-theme="monokai"]
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
.appId
|
||||
color $ui-dracula-text-color
|
||||
.list
|
||||
a
|
||||
color $ui-dracula-active-color
|
||||
color $ui-dracula-active-color
|
||||
@@ -4,6 +4,7 @@ import _ from 'lodash'
|
||||
import styles from './SnippetTab.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import snippetManager from '../../../lib/SnippetManager'
|
||||
|
||||
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
const buildCMRulers = (rulers, enableRulers) =>
|
||||
@@ -64,7 +65,9 @@ class SnippetEditor extends React.Component {
|
||||
}
|
||||
|
||||
saveSnippet () {
|
||||
dataApi.updateSnippet(this.snippet).catch((err) => { throw err })
|
||||
dataApi.updateSnippet(this.snippet)
|
||||
.then(snippets => snippetManager.assignSnippets(snippets))
|
||||
.catch((err) => { throw err })
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
@@ -91,7 +91,7 @@ class SnippetTab extends React.Component {
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='header'>{i18n.__('Snippets')}</div>
|
||||
<div styleName='group-header'>{i18n.__('Snippets')}</div>
|
||||
<SnippetList
|
||||
onSnippetSelect={this.handleSnippetSelect.bind(this)}
|
||||
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
@import('./Tab')
|
||||
@import('./ConfigTab')
|
||||
|
||||
.root
|
||||
padding 15px
|
||||
white-space pre
|
||||
line-height 1.4
|
||||
color alpha($ui-text-color, 90%)
|
||||
width 100%
|
||||
font-size 14px
|
||||
|
||||
.group
|
||||
margin-bottom 45px
|
||||
|
||||
@@ -127,7 +118,7 @@
|
||||
background darken(#f5f5f5, 5)
|
||||
|
||||
.snippet-detail
|
||||
width 70%
|
||||
width 67%
|
||||
height calc(100% - 200px)
|
||||
position absolute
|
||||
left 33%
|
||||
|
||||
@@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StorageItem.styl'
|
||||
import consts from 'browser/lib/consts'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import store from 'browser/main/store'
|
||||
import { store } from 'browser/main/store'
|
||||
import FolderList from './FolderList'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
@import('./Tab')
|
||||
|
||||
.root
|
||||
padding 15px
|
||||
color $ui-text-color
|
||||
@import('./ConfigTab')
|
||||
|
||||
.list
|
||||
margin-bottom 15px
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { store } from 'browser/main/store'
|
||||
import consts from 'browser/lib/consts'
|
||||
import ReactCodeMirror from 'react-codemirror'
|
||||
import CodeMirror from 'codemirror'
|
||||
@@ -30,7 +30,9 @@ class UiTab extends React.Component {
|
||||
componentDidMount () {
|
||||
CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript')
|
||||
CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css')
|
||||
CodeMirror.autoLoadMode(this.customMarkdownLintConfigCM.getCodeMirror(), 'javascript')
|
||||
this.customCSSCM.getCodeMirror().setSize('400px', '400px')
|
||||
this.customMarkdownLintConfigCM.getCodeMirror().setSize('400px', '200px')
|
||||
this.handleSettingDone = () => {
|
||||
this.setState({UiAlert: {
|
||||
type: 'success',
|
||||
@@ -101,7 +103,9 @@ class UiTab extends React.Component {
|
||||
matchingTriples: this.refs.matchingTriples.value,
|
||||
explodingPairs: this.refs.explodingPairs.value,
|
||||
spellcheck: this.refs.spellcheck.checked,
|
||||
enableSmartPaste: this.refs.enableSmartPaste.checked
|
||||
enableSmartPaste: this.refs.enableSmartPaste.checked,
|
||||
enableMarkdownLint: this.refs.enableMarkdownLint.checked,
|
||||
customMarkdownLintConfig: this.customMarkdownLintConfigCM.getCodeMirror().getValue()
|
||||
},
|
||||
preview: {
|
||||
fontSize: this.refs.previewFontSize.value,
|
||||
@@ -128,8 +132,13 @@ class UiTab extends React.Component {
|
||||
const newCodemirrorTheme = this.refs.editorTheme.value
|
||||
|
||||
if (newCodemirrorTheme !== codemirrorTheme) {
|
||||
checkHighLight.setAttribute('href', `../node_modules/codemirror/theme/${newCodemirrorTheme.split(' ')[0]}.css`)
|
||||
const theme = consts.THEMES.find(theme => theme.name === newCodemirrorTheme)
|
||||
|
||||
if (theme) {
|
||||
checkHighLight.setAttribute('href', `../${theme.path}`)
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ config: newConfig, codemirrorTheme: newCodemirrorTheme }, () => {
|
||||
const {ui, editor, preview} = this.props.config
|
||||
this.currentConfig = {ui, editor, preview}
|
||||
@@ -355,7 +364,7 @@ class UiTab extends React.Component {
|
||||
>
|
||||
{
|
||||
themes.map((theme) => {
|
||||
return (<option value={theme} key={theme}>{theme}</option>)
|
||||
return (<option value={theme.name} key={theme.name}>{theme.name}</option>)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
@@ -492,7 +501,7 @@ class UiTab extends React.Component {
|
||||
ref='editorSnippetDefaultLanguage'
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
>
|
||||
<option key='Auto Detect' value='Auto Detect'>Auto Detect</option>
|
||||
<option key='Auto Detect' value='Auto Detect'>{i18n.__('Auto Detect')}</option>
|
||||
{
|
||||
_.sortBy(CodeMirror.modeInfo.map(mode => mode.name)).map(name => (<option key={name} value={name}>{name}</option>))
|
||||
}
|
||||
@@ -632,6 +641,34 @@ class UiTab extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Custom MarkdownLint Rules')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.enableMarkdownLint}
|
||||
ref='enableMarkdownLint'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Enable MarkdownLint')}
|
||||
<div style={{fontFamily, display: this.state.config.editor.enableMarkdownLint ? 'block' : 'none'}}>
|
||||
<ReactCodeMirror
|
||||
width='400px'
|
||||
height='200px'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
ref={e => (this.customMarkdownLintConfigCM = e)}
|
||||
value={config.editor.customMarkdownLintConfig}
|
||||
options={{
|
||||
lineNumbers: true,
|
||||
mode: 'application/json',
|
||||
theme: codemirrorTheme,
|
||||
lint: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers']
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-header2'>{i18n.__('Preview')}</div>
|
||||
<div styleName='group-section'>
|
||||
@@ -670,7 +707,7 @@ class UiTab extends React.Component {
|
||||
>
|
||||
{
|
||||
themes.map((theme) => {
|
||||
return (<option value={theme} key={theme}>{theme}</option>)
|
||||
return (<option value={theme.name} key={theme.name}>{theme.name}</option>)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
@@ -846,6 +883,7 @@ class UiTab extends React.Component {
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
ref={e => (this.customCSSCM = e)}
|
||||
value={config.preview.customCSS}
|
||||
defaultValue={'/* Drop Your Custom CSS Code Here */\n'}
|
||||
options={{
|
||||
lineNumbers: true,
|
||||
mode: 'css',
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './RenameFolderModal.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import store from 'browser/main/store'
|
||||
import { store } from 'browser/main/store'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { combineReducers, createStore } from 'redux'
|
||||
import { routerReducer } from 'react-router-redux'
|
||||
import { combineReducers, createStore, compose, applyMiddleware } from 'redux'
|
||||
import { connectRouter, routerMiddleware } from 'connected-react-router'
|
||||
import { createHashHistory as createHistory } from 'history'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import { Map, Set } from 'browser/lib/Mutable'
|
||||
import _ from 'lodash'
|
||||
import DevTools from './DevTools'
|
||||
|
||||
function defaultDataMap () {
|
||||
return {
|
||||
@@ -44,7 +46,9 @@ function data (state = defaultDataMap(), action) {
|
||||
const folderNoteSet = getOrInitItem(state.folderNoteMap, folderKey)
|
||||
folderNoteSet.add(uniqueKey)
|
||||
|
||||
assignToTags(note.tags, state, uniqueKey)
|
||||
if (!note.isTrashed) {
|
||||
assignToTags(note.tags, state, uniqueKey)
|
||||
}
|
||||
})
|
||||
return state
|
||||
case 'UPDATE_NOTE':
|
||||
@@ -463,13 +467,16 @@ function getOrInitItem (target, key) {
|
||||
return results
|
||||
}
|
||||
|
||||
const history = createHistory()
|
||||
|
||||
const reducer = combineReducers({
|
||||
data,
|
||||
config,
|
||||
status,
|
||||
routing: routerReducer
|
||||
router: connectRouter(history)
|
||||
})
|
||||
|
||||
const store = createStore(reducer)
|
||||
const store = createStore(reducer, undefined, compose(
|
||||
applyMiddleware(routerMiddleware(history)), DevTools.instrument()))
|
||||
|
||||
export default store
|
||||
export { store, history }
|
||||
|
||||
Reference in New Issue
Block a user