mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-12 17:26:17 +00:00
fixed eslint error & integrated with prettier as well as formatted the whole codebase (#3450)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": ["standard", "standard-jsx", "plugin:react/recommended"],
|
"extends": ["standard", "standard-jsx", "plugin:react/recommended", "prettier"],
|
||||||
"plugins": ["react"],
|
"plugins": ["react", "prettier"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-useless-escape": 0,
|
"no-useless-escape": 0,
|
||||||
"prefer-const": ["warn", {
|
"prefer-const": ["warn", {
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"react/no-string-refs": 0,
|
"react/no-string-refs": 0,
|
||||||
"react/no-find-dom-node": "warn",
|
"react/no-find-dom-node": "warn",
|
||||||
"react/no-render-return-value": "warn",
|
"react/no-render-return-value": "warn",
|
||||||
"react/no-deprecated": "warn"
|
"react/no-deprecated": "warn",
|
||||||
|
"prettier/prettier": ["error"]
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"FileReader": true,
|
"FileReader": true,
|
||||||
|
|||||||
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"jsxSingleQuote": true
|
||||||
|
}
|
||||||
@@ -6,11 +6,7 @@ import hljs from 'highlight.js'
|
|||||||
import 'codemirror-mode-elixir'
|
import 'codemirror-mode-elixir'
|
||||||
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||||
import convertModeName from 'browser/lib/convertModeName'
|
import convertModeName from 'browser/lib/convertModeName'
|
||||||
import {
|
import { options, TableEditor, Alignment } from '@susisu/mte-kernel'
|
||||||
options,
|
|
||||||
TableEditor,
|
|
||||||
Alignment
|
|
||||||
} from '@susisu/mte-kernel'
|
|
||||||
import TextEditorInterface from 'browser/lib/TextEditorInterface'
|
import TextEditorInterface from 'browser/lib/TextEditorInterface'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import iconv from 'iconv-lite'
|
import iconv from 'iconv-lite'
|
||||||
@@ -20,11 +16,15 @@ import styles from '../components/CodeEditor.styl'
|
|||||||
const { ipcRenderer, remote, clipboard } = require('electron')
|
const { ipcRenderer, remote, clipboard } = require('electron')
|
||||||
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||||
const spellcheck = require('browser/lib/spellcheck')
|
const spellcheck = require('browser/lib/spellcheck')
|
||||||
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu
|
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
|
||||||
|
.buildEditorContextMenu
|
||||||
import { createTurndownService } from '../lib/turndown'
|
import { createTurndownService } from '../lib/turndown'
|
||||||
import { languageMaps } from '../lib/CMLanguageList'
|
import { languageMaps } from '../lib/CMLanguageList'
|
||||||
import snippetManager from '../lib/SnippetManager'
|
import snippetManager from '../lib/SnippetManager'
|
||||||
import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator'
|
import {
|
||||||
|
generateInEditor,
|
||||||
|
tocExistsInEditor
|
||||||
|
} from 'browser/lib/markdown-toc-generator'
|
||||||
import markdownlint from 'markdownlint'
|
import markdownlint from 'markdownlint'
|
||||||
import Jsonlint from 'jsonlint-mod'
|
import Jsonlint from 'jsonlint-mod'
|
||||||
import { DEFAULT_CONFIG } from '../main/lib/ConfigManager'
|
import { DEFAULT_CONFIG } from '../main/lib/ConfigManager'
|
||||||
@@ -33,12 +33,17 @@ import prettier from 'prettier'
|
|||||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||||
|
|
||||||
const buildCMRulers = (rulers, enableRulers) =>
|
const buildCMRulers = (rulers, enableRulers) =>
|
||||||
(enableRulers ? rulers.map(ruler => ({
|
enableRulers
|
||||||
|
? rulers.map(ruler => ({
|
||||||
column: ruler
|
column: ruler
|
||||||
})) : [])
|
}))
|
||||||
|
: []
|
||||||
|
|
||||||
function translateHotkey(hotkey) {
|
function translateHotkey(hotkey) {
|
||||||
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
|
return hotkey
|
||||||
|
.replace(/\s*\+\s*/g, '-')
|
||||||
|
.replace(/Command/g, 'Cmd')
|
||||||
|
.replace(/Control/g, 'Ctrl')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CodeEditor extends React.Component {
|
export default class CodeEditor extends React.Component {
|
||||||
@@ -49,12 +54,17 @@ export default class CodeEditor extends React.Component {
|
|||||||
leading: false,
|
leading: false,
|
||||||
trailing: true
|
trailing: true
|
||||||
})
|
})
|
||||||
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
|
this.changeHandler = (editor, changeObject) =>
|
||||||
this.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject)
|
this.handleChange(editor, changeObject)
|
||||||
|
this.highlightHandler = (editor, changeObject) =>
|
||||||
|
this.handleHighlight(editor, changeObject)
|
||||||
this.focusHandler = () => {
|
this.focusHandler = () => {
|
||||||
ipcRenderer.send('editor:focused', true)
|
ipcRenderer.send('editor:focused', true)
|
||||||
}
|
}
|
||||||
const debouncedDeletionOfAttachments = _.debounce(attachmentManagement.deleteAttachmentsNotPresentInNote, 30000)
|
const debouncedDeletionOfAttachments = _.debounce(
|
||||||
|
attachmentManagement.deleteAttachmentsNotPresentInNote,
|
||||||
|
30000
|
||||||
|
)
|
||||||
this.blurHandler = (editor, e) => {
|
this.blurHandler = (editor, e) => {
|
||||||
ipcRenderer.send('editor:focused', false)
|
ipcRenderer.send('editor:focused', false)
|
||||||
if (e == null) return null
|
if (e == null) return null
|
||||||
@@ -66,12 +76,13 @@ export default class CodeEditor extends React.Component {
|
|||||||
el = el.parentNode
|
el = el.parentNode
|
||||||
}
|
}
|
||||||
this.props.onBlur != null && this.props.onBlur(e)
|
this.props.onBlur != null && this.props.onBlur(e)
|
||||||
const {
|
const { storageKey, noteKey } = this.props
|
||||||
|
if (this.props.deleteUnusedAttachments === true) {
|
||||||
|
debouncedDeletionOfAttachments(
|
||||||
|
this.editor.getValue(),
|
||||||
storageKey,
|
storageKey,
|
||||||
noteKey
|
noteKey
|
||||||
} = this.props
|
)
|
||||||
if (this.props.deleteUnusedAttachments === true) {
|
|
||||||
debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.pasteHandler = (editor, e) => {
|
this.pasteHandler = (editor, e) => {
|
||||||
@@ -139,9 +150,11 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFormatTable() {
|
handleFormatTable() {
|
||||||
this.tableEditor.formatAll(options({
|
this.tableEditor.formatAll(
|
||||||
|
options({
|
||||||
textWidthOptions: {}
|
textWidthOptions: {}
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditorActivity() {
|
handleEditorActivity() {
|
||||||
@@ -231,7 +244,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos)
|
currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos)
|
||||||
|
|
||||||
// Prettify contents of editor
|
// Prettify contents of editor
|
||||||
const formattedTextDetails = prettier.formatWithCursor(cm.doc.getValue(), currentConfig)
|
const formattedTextDetails = prettier.formatWithCursor(
|
||||||
|
cm.doc.getValue(),
|
||||||
|
currentConfig
|
||||||
|
)
|
||||||
|
|
||||||
const formattedText = formattedTextDetails.formatted
|
const formattedText = formattedTextDetails.formatted
|
||||||
const formattedCursorPos = formattedTextDetails.cursorOffset
|
const formattedCursorPos = formattedTextDetails.cursorOffset
|
||||||
@@ -246,7 +262,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
const appendLineBreak = /\n$/.test(selection)
|
const appendLineBreak = /\n$/.test(selection)
|
||||||
|
|
||||||
const sorted = _.split(selection.trim(), '\n').sort()
|
const sorted = _.split(selection.trim(), '\n').sort()
|
||||||
const sortedString = _.join(sorted, '\n') + (appendLineBreak ? '\n' : '')
|
const sortedString =
|
||||||
|
_.join(sorted, '\n') + (appendLineBreak ? '\n' : '')
|
||||||
|
|
||||||
cm.doc.replaceSelection(sortedString)
|
cm.doc.replaceSelection(sortedString)
|
||||||
},
|
},
|
||||||
@@ -298,7 +315,11 @@ export default class CodeEditor extends React.Component {
|
|||||||
rtlMoveVisually: RTL,
|
rtlMoveVisually: RTL,
|
||||||
foldGutter: true,
|
foldGutter: true,
|
||||||
lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false,
|
lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false,
|
||||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
gutters: [
|
||||||
|
'CodeMirror-linenumbers',
|
||||||
|
'CodeMirror-foldgutter',
|
||||||
|
'CodeMirror-lint-markers'
|
||||||
|
],
|
||||||
autoCloseBrackets: {
|
autoCloseBrackets: {
|
||||||
pairs: this.props.matchingPairs,
|
pairs: this.props.matchingPairs,
|
||||||
triples: this.props.matchingTriples,
|
triples: this.props.matchingTriples,
|
||||||
@@ -309,7 +330,9 @@ export default class CodeEditor extends React.Component {
|
|||||||
prettierConfig: this.props.prettierConfig
|
prettierConfig: this.props.prettierConfig
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none'
|
document.querySelector(
|
||||||
|
'.CodeMirror-lint-markers'
|
||||||
|
).style.display = enableMarkdownLint ? 'inline-block' : 'none'
|
||||||
|
|
||||||
if (!this.props.mode && this.props.value && this.props.autoDetect) {
|
if (!this.props.mode && this.props.value && this.props.autoDetect) {
|
||||||
this.autoDetectLanguage(this.props.value)
|
this.autoDetectLanguage(this.props.value)
|
||||||
@@ -352,13 +375,13 @@ export default class CodeEditor extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.editorKeyMap = CodeMirror.normalizeKeyMap({
|
this.editorKeyMap = CodeMirror.normalizeKeyMap({
|
||||||
'Tab': () => {
|
Tab: () => {
|
||||||
this.tableEditor.nextCell(this.tableEditorOptions)
|
this.tableEditor.nextCell(this.tableEditorOptions)
|
||||||
},
|
},
|
||||||
'Shift-Tab': () => {
|
'Shift-Tab': () => {
|
||||||
this.tableEditor.previousCell(this.tableEditorOptions)
|
this.tableEditor.previousCell(this.tableEditorOptions)
|
||||||
},
|
},
|
||||||
'Enter': () => {
|
Enter: () => {
|
||||||
this.tableEditor.nextRow(this.tableEditorOptions)
|
this.tableEditor.nextRow(this.tableEditorOptions)
|
||||||
},
|
},
|
||||||
'Ctrl-Enter': () => {
|
'Ctrl-Enter': () => {
|
||||||
@@ -561,13 +584,18 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.setOption('direction', this.props.RTL ? 'rtl' : 'ltr')
|
this.editor.setOption('direction', this.props.RTL ? 'rtl' : 'ltr')
|
||||||
this.editor.setOption('rtlMoveVisually', this.props.RTL)
|
this.editor.setOption('rtlMoveVisually', this.props.RTL)
|
||||||
}
|
}
|
||||||
if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) {
|
if (
|
||||||
|
prevProps.enableMarkdownLint !== enableMarkdownLint ||
|
||||||
|
prevProps.customMarkdownLintConfig !== customMarkdownLintConfig
|
||||||
|
) {
|
||||||
if (!enableMarkdownLint) {
|
if (!enableMarkdownLint) {
|
||||||
this.editor.setOption('lint', { default: false })
|
this.editor.setOption('lint', { default: false })
|
||||||
document.querySelector('.CodeMirror-lint-markers').style.display = 'none'
|
document.querySelector('.CodeMirror-lint-markers').style.display =
|
||||||
|
'none'
|
||||||
} else {
|
} else {
|
||||||
this.editor.setOption('lint', this.getCodeEditorLintConfig())
|
this.editor.setOption('lint', this.getCodeEditorLintConfig())
|
||||||
document.querySelector('.CodeMirror-lint-markers').style.display = 'inline-block'
|
document.querySelector('.CodeMirror-lint-markers').style.display =
|
||||||
|
'inline-block'
|
||||||
}
|
}
|
||||||
needRefresh = true
|
needRefresh = true
|
||||||
}
|
}
|
||||||
@@ -599,9 +627,11 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
|
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevProps.matchingPairs !== this.props.matchingPairs ||
|
if (
|
||||||
|
prevProps.matchingPairs !== this.props.matchingPairs ||
|
||||||
prevProps.matchingTriples !== this.props.matchingTriples ||
|
prevProps.matchingTriples !== this.props.matchingTriples ||
|
||||||
prevProps.explodingPairs !== this.props.explodingPairs) {
|
prevProps.explodingPairs !== this.props.explodingPairs
|
||||||
|
) {
|
||||||
const bracketObject = {
|
const bracketObject = {
|
||||||
pairs: this.props.matchingPairs,
|
pairs: this.props.matchingPairs,
|
||||||
triples: this.props.matchingTriples,
|
triples: this.props.matchingTriples,
|
||||||
@@ -646,11 +676,18 @@ export default class CodeEditor extends React.Component {
|
|||||||
const elem = document.getElementById('editor-bottom-panel')
|
const elem = document.getElementById('editor-bottom-panel')
|
||||||
elem.parentNode.removeChild(elem)
|
elem.parentNode.removeChild(elem)
|
||||||
} else {
|
} else {
|
||||||
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
|
this.editor.addPanel(this.createSpellCheckPanel(), {
|
||||||
|
position: 'bottom'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments) {
|
if (
|
||||||
this.editor.setOption('deleteUnusedAttachments', this.props.deleteUnusedAttachments)
|
prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments
|
||||||
|
) {
|
||||||
|
this.editor.setOption(
|
||||||
|
'deleteUnusedAttachments',
|
||||||
|
this.props.deleteUnusedAttachments
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needRefresh) {
|
if (needRefresh) {
|
||||||
@@ -662,10 +699,12 @@ export default class CodeEditor extends React.Component {
|
|||||||
const { mode } = this.props
|
const { mode } = this.props
|
||||||
const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown'
|
const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown'
|
||||||
|
|
||||||
return checkMarkdownNoteIsOpen ? {
|
return checkMarkdownNoteIsOpen
|
||||||
|
? {
|
||||||
getAnnotations: this.validatorOfMarkdown,
|
getAnnotations: this.validatorOfMarkdown,
|
||||||
async: true
|
async: true
|
||||||
} : false
|
}
|
||||||
|
: false
|
||||||
}
|
}
|
||||||
|
|
||||||
validatorOfMarkdown(text, updateLinting) {
|
validatorOfMarkdown(text, updateLinting) {
|
||||||
@@ -693,7 +732,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
let ruleNames = ''
|
let ruleNames = ''
|
||||||
item.ruleNames.map((ruleName, index) => {
|
item.ruleNames.map((ruleName, index) => {
|
||||||
ruleNames += ruleName
|
ruleNames += ruleName
|
||||||
ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/'
|
ruleNames += index === item.ruleNames.length - 1 ? ': ' : '/'
|
||||||
})
|
})
|
||||||
const lineNumber = item.lineNumber - 1
|
const lineNumber = item.lineNumber - 1
|
||||||
foundIssues.push({
|
foundIssues.push({
|
||||||
@@ -726,7 +765,11 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
// Check if one of the changed lines contains a headline
|
// Check if one of the changed lines contains a headline
|
||||||
for (let line = 0; line < changeObject.text.length; line++) {
|
for (let line = 0; line < changeObject.text.length; line++) {
|
||||||
if (this.linePossibleContainsHeadline(editor.getLine(changeObject.from.line + line))) {
|
if (
|
||||||
|
this.linePossibleContainsHeadline(
|
||||||
|
editor.getLine(changeObject.from.line + line)
|
||||||
|
)
|
||||||
|
) {
|
||||||
requireTocUpdate = true
|
requireTocUpdate = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -782,7 +825,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1)
|
highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1)
|
||||||
|
|
||||||
// Lines that need to be relocated
|
// Lines that need to be relocated
|
||||||
if (lineNumber >= (start + linesRemoved)) {
|
if (lineNumber >= start + linesRemoved) {
|
||||||
newLines.push(lineNumber + offset)
|
newLines.push(lineNumber + offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -801,10 +844,18 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
if (!lines.includes(changeObject)) {
|
if (!lines.includes(changeObject)) {
|
||||||
lines.push(changeObject)
|
lines.push(changeObject)
|
||||||
editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
|
editor.addLineClass(
|
||||||
|
changeObject,
|
||||||
|
'text',
|
||||||
|
'CodeMirror-activeline-background'
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
lines.splice(lines.indexOf(changeObject), 1)
|
lines.splice(lines.indexOf(changeObject), 1)
|
||||||
editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
|
editor.removeLineClass(
|
||||||
|
changeObject,
|
||||||
|
'text',
|
||||||
|
'CodeMirror-activeline-background'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (this.props.onChange) {
|
if (this.props.onChange) {
|
||||||
this.props.onChange(editor)
|
this.props.onChange(editor)
|
||||||
@@ -888,15 +939,16 @@ export default class CodeEditor extends React.Component {
|
|||||||
setLineContent(lineNumber, content) {
|
setLineContent(lineNumber, content) {
|
||||||
const prevContent = this.editor.getLine(lineNumber)
|
const prevContent = this.editor.getLine(lineNumber)
|
||||||
const prevContentLength = prevContent ? prevContent.length : 0
|
const prevContentLength = prevContent ? prevContent.length : 0
|
||||||
this.editor.replaceRange(content, { line: lineNumber, ch: 0 }, { line: lineNumber, ch: prevContentLength })
|
this.editor.replaceRange(
|
||||||
|
content,
|
||||||
|
{ line: lineNumber, ch: 0 },
|
||||||
|
{ line: lineNumber, ch: prevContentLength }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropImage(dropEvent) {
|
handleDropImage(dropEvent) {
|
||||||
dropEvent.preventDefault()
|
dropEvent.preventDefault()
|
||||||
const {
|
const { storageKey, noteKey } = this.props
|
||||||
storageKey,
|
|
||||||
noteKey
|
|
||||||
} = this.props
|
|
||||||
attachmentManagement.handleAttachmentDrop(
|
attachmentManagement.handleAttachmentDrop(
|
||||||
this,
|
this,
|
||||||
storageKey,
|
storageKey,
|
||||||
@@ -917,25 +969,32 @@ export default class CodeEditor extends React.Component {
|
|||||||
handlePaste(editor, forceSmartPaste) {
|
handlePaste(editor, forceSmartPaste) {
|
||||||
const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
|
const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
|
||||||
|
|
||||||
const isURL = str => /(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
|
const isURL = str =>
|
||||||
|
/(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
|
||||||
|
|
||||||
const isInLinkTag = editor => {
|
const isInLinkTag = editor => {
|
||||||
const startCursor = editor.getCursor('start')
|
const startCursor = editor.getCursor('start')
|
||||||
const prevChar = editor.getRange({
|
const prevChar = editor.getRange(
|
||||||
|
{
|
||||||
line: startCursor.line,
|
line: startCursor.line,
|
||||||
ch: startCursor.ch - 2
|
ch: startCursor.ch - 2
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
line: startCursor.line,
|
line: startCursor.line,
|
||||||
ch: startCursor.ch
|
ch: startCursor.ch
|
||||||
})
|
}
|
||||||
|
)
|
||||||
const endCursor = editor.getCursor('end')
|
const endCursor = editor.getCursor('end')
|
||||||
const nextChar = editor.getRange({
|
const nextChar = editor.getRange(
|
||||||
|
{
|
||||||
line: endCursor.line,
|
line: endCursor.line,
|
||||||
ch: endCursor.ch
|
ch: endCursor.ch
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
line: endCursor.line,
|
line: endCursor.line,
|
||||||
ch: endCursor.ch + 1
|
ch: endCursor.ch + 1
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return prevChar === '](' && nextChar === ')'
|
return prevChar === '](' && nextChar === ')'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -947,7 +1006,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
let line = line = cursor.line - 1
|
let line = (line = cursor.line - 1)
|
||||||
while (line >= 0) {
|
while (line >= 0) {
|
||||||
token = editor.getTokenAt({
|
token = editor.getTokenAt({
|
||||||
ch: 3,
|
ch: 3,
|
||||||
@@ -979,7 +1038,11 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
if (isInFencedCodeBlock(editor)) {
|
if (isInFencedCodeBlock(editor)) {
|
||||||
this.handlePasteText(editor, pastedTxt)
|
this.handlePasteText(editor, pastedTxt)
|
||||||
} else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) {
|
} else if (
|
||||||
|
fetchUrlTitle &&
|
||||||
|
isMarkdownTitleURL(pastedTxt) &&
|
||||||
|
!isInLinkTag(editor)
|
||||||
|
) {
|
||||||
this.handlePasteUrl(editor, pastedTxt)
|
this.handlePasteUrl(editor, pastedTxt)
|
||||||
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||||
this.handlePasteUrl(editor, pastedTxt)
|
this.handlePasteUrl(editor, pastedTxt)
|
||||||
@@ -1088,10 +1151,12 @@ export default class CodeEditor extends React.Component {
|
|||||||
body,
|
body,
|
||||||
'text/html'
|
'text/html'
|
||||||
)
|
)
|
||||||
const escapePipe = (str) => {
|
const escapePipe = str => {
|
||||||
return str.replace('|', '\\|')
|
return str.replace('|', '\\|')
|
||||||
}
|
}
|
||||||
const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})`
|
const linkWithTitle = `[${escapePipe(
|
||||||
|
parsedBody.title
|
||||||
|
)}](${pastedTxt})`
|
||||||
resolve(linkWithTitle)
|
resolve(linkWithTitle)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e)
|
reject(e)
|
||||||
@@ -1114,7 +1179,11 @@ export default class CodeEditor extends React.Component {
|
|||||||
// make sure that we skip the invalid lines althrough this case should not be happened.
|
// make sure that we skip the invalid lines althrough this case should not be happened.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background')
|
this.editor.addLineClass(
|
||||||
|
lineNumber,
|
||||||
|
'text',
|
||||||
|
'CodeMirror-activeline-background'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,8 +1213,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
return response.arrayBuffer().then(buff => {
|
return response.arrayBuffer().then(buff => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const charset = _charset !== undefined &&
|
const charset =
|
||||||
iconv.encodingExists(_charset)
|
_charset !== undefined && iconv.encodingExists(_charset)
|
||||||
? _charset
|
? _charset
|
||||||
: 'utf-8'
|
: 'utf-8'
|
||||||
resolve(iconv.decode(Buffer.from(buff), charset).toString())
|
resolve(iconv.decode(Buffer.from(buff), charset).toString())
|
||||||
@@ -1160,7 +1229,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
return contentType
|
return contentType
|
||||||
.split(';')
|
.split(';')
|
||||||
.filter(str => {
|
.filter(str => {
|
||||||
return str.trim().toLowerCase().startsWith('charset')
|
return str
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.startsWith('charset')
|
||||||
})
|
})
|
||||||
.map(str => {
|
.map(str => {
|
||||||
return str.replace(/['"]/g, '').split('=')[1]
|
return str.replace(/['"]/g, '').split('=')[1]
|
||||||
@@ -1168,16 +1240,12 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { className, fontSize } = this.props
|
||||||
className,
|
|
||||||
fontSize
|
|
||||||
} = this.props
|
|
||||||
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
|
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
|
||||||
const width = this.props.width
|
const width = this.props.width
|
||||||
return (<
|
return (
|
||||||
div className={
|
<div
|
||||||
className == null ? 'CodeEditor' : `CodeEditor ${className}`
|
className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
|
||||||
}
|
|
||||||
ref='root'
|
ref='root'
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
style={{
|
style={{
|
||||||
@@ -1185,9 +1253,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
width: width
|
width: width
|
||||||
}}
|
}}
|
||||||
onDrop={
|
onDrop={e => this.handleDropImage(e)}
|
||||||
e => this.handleDropImage(e)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1199,7 +1265,9 @@ export default class CodeEditor extends React.Component {
|
|||||||
const dropdown = document.createElement('select')
|
const dropdown = document.createElement('select')
|
||||||
dropdown.title = 'Spellcheck'
|
dropdown.title = 'Spellcheck'
|
||||||
dropdown.className = styles['spellcheck-select']
|
dropdown.className = styles['spellcheck-select']
|
||||||
dropdown.addEventListener('change', (e) => spellcheck.setLanguage(this.editor, dropdown.value))
|
dropdown.addEventListener('change', e =>
|
||||||
|
spellcheck.setLanguage(this.editor, dropdown.value)
|
||||||
|
)
|
||||||
const options = spellcheck.getAvailableDictionaries()
|
const options = spellcheck.getAvailableDictionaries()
|
||||||
for (const op of options) {
|
for (const op of options) {
|
||||||
const option = document.createElement('option')
|
const option = document.createElement('option')
|
||||||
|
|||||||
@@ -44,13 +44,22 @@ class ColorPicker extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div styleName='colorPicker' style={{top: `${alignY}px`, left: `${alignX}px`}}>
|
<div
|
||||||
|
styleName='colorPicker'
|
||||||
|
style={{ top: `${alignY}px`, left: `${alignX}px` }}
|
||||||
|
>
|
||||||
<div styleName='cover' onClick={onCancel} />
|
<div styleName='cover' onClick={onCancel} />
|
||||||
<SketchPicker color={color} onChange={this.onColorChange} />
|
<SketchPicker color={color} onChange={this.onColorChange} />
|
||||||
<div styleName='footer'>
|
<div styleName='footer'>
|
||||||
<button styleName='btn-reset' onClick={onReset}>Reset</button>
|
<button styleName='btn-reset' onClick={onReset}>
|
||||||
<button styleName='btn-cancel' onClick={onCancel}>Cancel</button>
|
Reset
|
||||||
<button styleName='btn-confirm' onClick={this.handleConfirm}>Confirm</button>
|
</button>
|
||||||
|
<button styleName='btn-cancel' onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button styleName='btn-confirm' onClick={this.handleConfirm}>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ class MarkdownEditor extends React.Component {
|
|||||||
this.supportMdSelectionBold = [16, 17, 186]
|
this.supportMdSelectionBold = [16, 17, 186]
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE',
|
status:
|
||||||
|
props.config.editor.switchPreview === 'RIGHTCLICK'
|
||||||
|
? props.config.editor.delfaultStatus
|
||||||
|
: 'CODE',
|
||||||
renderValue: props.value,
|
renderValue: props.value,
|
||||||
keyPressed: new Set(),
|
keyPressed: new Set(),
|
||||||
isLocked: props.isLocked
|
isLocked: props.isLocked
|
||||||
@@ -52,11 +55,14 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusEditor() {
|
focusEditor() {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'CODE'
|
status: 'CODE'
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.code.focus()
|
this.refs.code.focus()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
queueRendering(value) {
|
queueRendering(value) {
|
||||||
@@ -90,9 +96,11 @@ class MarkdownEditor extends React.Component {
|
|||||||
const { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||||
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
|
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: newStatus
|
status: newStatus
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
if (newStatus === 'CODE') {
|
if (newStatus === 'CODE') {
|
||||||
this.refs.code.focus()
|
this.refs.code.focus()
|
||||||
} else {
|
} else {
|
||||||
@@ -103,7 +111,8 @@ class MarkdownEditor extends React.Component {
|
|||||||
const newConfig = Object.assign({}, config)
|
const newConfig = Object.assign({}, config)
|
||||||
newConfig.editor.delfaultStatus = newStatus
|
newConfig.editor.delfaultStatus = newStatus
|
||||||
ConfigManager.set(newConfig)
|
ConfigManager.set(newConfig)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,16 +120,21 @@ class MarkdownEditor extends React.Component {
|
|||||||
if (this.state.isLocked) return
|
if (this.state.isLocked) return
|
||||||
this.setState({ keyPressed: new Set() })
|
this.setState({ keyPressed: new Set() })
|
||||||
const { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'BLUR' ||
|
if (
|
||||||
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
|
config.editor.switchPreview === 'BLUR' ||
|
||||||
|
(config.editor.switchPreview === 'DBL_CLICK' &&
|
||||||
|
this.state.status === 'CODE')
|
||||||
) {
|
) {
|
||||||
const cursorPosition = this.refs.code.editor.getCursor()
|
const cursorPosition = this.refs.code.editor.getCursor()
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'PREVIEW'
|
status: 'PREVIEW'
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.preview.focus()
|
this.refs.preview.focus()
|
||||||
this.refs.preview.scrollToRow(cursorPosition.line)
|
this.refs.preview.scrollToRow(cursorPosition.line)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,12 +144,15 @@ class MarkdownEditor extends React.Component {
|
|||||||
this.setState({ keyPressed: new Set() })
|
this.setState({ keyPressed: new Set() })
|
||||||
const { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'DBL_CLICK') {
|
if (config.editor.switchPreview === 'DBL_CLICK') {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'CODE'
|
status: 'CODE'
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.code.focus()
|
this.refs.code.focus()
|
||||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,12 +162,18 @@ class MarkdownEditor extends React.Component {
|
|||||||
|
|
||||||
handlePreviewMouseUp(e) {
|
handlePreviewMouseUp(e) {
|
||||||
const { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
|
if (
|
||||||
this.setState({
|
config.editor.switchPreview === 'BLUR' &&
|
||||||
|
new Date() - this.previewMouseDownedAt < 200
|
||||||
|
) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
status: 'CODE'
|
status: 'CODE'
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.code.focus()
|
this.refs.code.focus()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,9 +187,9 @@ class MarkdownEditor extends React.Component {
|
|||||||
const checkReplace = /\[x]/i
|
const checkReplace = /\[x]/i
|
||||||
const uncheckReplace = /\[ ]/
|
const uncheckReplace = /\[ ]/
|
||||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
const lineIndex =
|
||||||
const lines = this.refs.code.value
|
parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
.split('\n')
|
const lines = this.refs.code.value.split('\n')
|
||||||
|
|
||||||
const targetLine = lines[lineIndex]
|
const targetLine = lines[lineIndex]
|
||||||
let newLine = targetLine
|
let newLine = targetLine
|
||||||
@@ -183,11 +206,14 @@ class MarkdownEditor extends React.Component {
|
|||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
if (this.state.status === 'PREVIEW') {
|
if (this.state.status === 'PREVIEW') {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'CODE'
|
status: 'CODE'
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.code.focus()
|
this.refs.code.focus()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
this.refs.code.focus()
|
this.refs.code.focus()
|
||||||
}
|
}
|
||||||
@@ -206,15 +232,23 @@ class MarkdownEditor extends React.Component {
|
|||||||
const keyPressed = this.state.keyPressed
|
const keyPressed = this.state.keyPressed
|
||||||
keyPressed.add(e.keyCode)
|
keyPressed.add(e.keyCode)
|
||||||
this.setState({ keyPressed })
|
this.setState({ keyPressed })
|
||||||
const isNoteHandlerKey = (el) => { return keyPressed.has(el) }
|
const isNoteHandlerKey = el => {
|
||||||
|
return keyPressed.has(el)
|
||||||
|
}
|
||||||
// These conditions are for ctrl-e and ctrl-w
|
// These conditions are for ctrl-e and ctrl-w
|
||||||
if (keyPressed.size === this.escapeFromEditor.length &&
|
if (
|
||||||
!this.state.isLocked && this.state.status === 'CODE' &&
|
keyPressed.size === this.escapeFromEditor.length &&
|
||||||
this.escapeFromEditor.every(isNoteHandlerKey)) {
|
!this.state.isLocked &&
|
||||||
|
this.state.status === 'CODE' &&
|
||||||
|
this.escapeFromEditor.every(isNoteHandlerKey)
|
||||||
|
) {
|
||||||
this.handleContextMenu()
|
this.handleContextMenu()
|
||||||
if (config.editor.switchPreview === 'BLUR') document.activeElement.blur()
|
if (config.editor.switchPreview === 'BLUR') document.activeElement.blur()
|
||||||
}
|
}
|
||||||
if (keyPressed.size === this.supportMdSelectionBold.length && this.supportMdSelectionBold.every(isNoteHandlerKey)) {
|
if (
|
||||||
|
keyPressed.size === this.supportMdSelectionBold.length &&
|
||||||
|
this.supportMdSelectionBold.every(isNoteHandlerKey)
|
||||||
|
) {
|
||||||
this.addMdAroundWord('**')
|
this.addMdAroundWord('**')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,20 +261,27 @@ class MarkdownEditor extends React.Component {
|
|||||||
const word = this.refs.code.editor.findWordAt(currentCaret)
|
const word = this.refs.code.editor.findWordAt(currentCaret)
|
||||||
const cmDoc = this.refs.code.editor.getDoc()
|
const cmDoc = this.refs.code.editor.getDoc()
|
||||||
cmDoc.replaceRange(mdElement, word.anchor)
|
cmDoc.replaceRange(mdElement, word.anchor)
|
||||||
cmDoc.replaceRange(mdElement, { line: word.head.line, ch: word.head.ch + mdElement.length })
|
cmDoc.replaceRange(mdElement, {
|
||||||
|
line: word.head.line,
|
||||||
|
ch: word.head.ch + mdElement.length
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addMdAroundSelection(mdElement) {
|
addMdAroundSelection(mdElement) {
|
||||||
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
|
this.refs.code.editor.replaceSelection(
|
||||||
|
`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropImage(dropEvent) {
|
handleDropImage(dropEvent) {
|
||||||
dropEvent.preventDefault()
|
dropEvent.preventDefault()
|
||||||
const { storageKey, noteKey } = this.props
|
const { storageKey, noteKey } = this.props
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'CODE'
|
status: 'CODE'
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.code.focus()
|
this.refs.code.focus()
|
||||||
|
|
||||||
this.refs.code.editor.execCommand('goDocEnd')
|
this.refs.code.editor.execCommand('goDocEnd')
|
||||||
@@ -253,7 +294,8 @@ class MarkdownEditor extends React.Component {
|
|||||||
noteKey,
|
noteKey,
|
||||||
dropEvent
|
dropEvent
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyUp(e) {
|
handleKeyUp(e) {
|
||||||
@@ -267,7 +309,15 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {className, value, config, storageKey, noteKey, linesHighlighted, RTL} = this.props
|
const {
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
config,
|
||||||
|
storageKey,
|
||||||
|
noteKey,
|
||||||
|
linesHighlighted,
|
||||||
|
RTL
|
||||||
|
} = this.props
|
||||||
|
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
@@ -275,23 +325,24 @@ class MarkdownEditor extends React.Component {
|
|||||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
|
|
||||||
const previewStyle = {}
|
const previewStyle = {}
|
||||||
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
if (this.props.ignorePreviewPointerEvents)
|
||||||
|
previewStyle.pointerEvents = 'none'
|
||||||
|
|
||||||
const storage = findStorage(storageKey)
|
const storage = findStorage(storageKey)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className == null
|
<div
|
||||||
? 'MarkdownEditor'
|
className={
|
||||||
: `MarkdownEditor ${className}`
|
className == null ? 'MarkdownEditor' : `MarkdownEditor ${className}`
|
||||||
}
|
}
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={e => this.handleContextMenu(e)}
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
onKeyDown={e => this.handleKeyDown(e)}
|
||||||
onKeyUp={(e) => this.handleKeyUp(e)}
|
onKeyUp={e => this.handleKeyUp(e)}
|
||||||
>
|
>
|
||||||
<CodeEditor styleName={this.state.status === 'CODE'
|
<CodeEditor
|
||||||
? 'codeEditor'
|
styleName={
|
||||||
: 'codeEditor--hide'
|
this.state.status === 'CODE' ? 'codeEditor' : 'codeEditor--hide'
|
||||||
}
|
}
|
||||||
ref='code'
|
ref='code'
|
||||||
mode='Boost Flavored Markdown'
|
mode='Boost Flavored Markdown'
|
||||||
@@ -315,8 +366,8 @@ class MarkdownEditor extends React.Component {
|
|||||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
enableTableEditor={config.editor.enableTableEditor}
|
enableTableEditor={config.editor.enableTableEditor}
|
||||||
linesHighlighted={linesHighlighted}
|
linesHighlighted={linesHighlighted}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={e => this.handleChange(e)}
|
||||||
onBlur={(e) => this.handleBlur(e)}
|
onBlur={e => this.handleBlur(e)}
|
||||||
spellCheck={config.editor.spellcheck}
|
spellCheck={config.editor.spellcheck}
|
||||||
enableSmartPaste={config.editor.enableSmartPaste}
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
hotkey={config.hotkey}
|
hotkey={config.hotkey}
|
||||||
@@ -327,9 +378,9 @@ class MarkdownEditor extends React.Component {
|
|||||||
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||||
RTL={RTL}
|
RTL={RTL}
|
||||||
/>
|
/>
|
||||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
<MarkdownPreview
|
||||||
? 'preview'
|
styleName={
|
||||||
: 'preview--hide'
|
this.state.status === 'PREVIEW' ? 'preview' : 'preview--hide'
|
||||||
}
|
}
|
||||||
style={previewStyle}
|
style={previewStyle}
|
||||||
theme={config.ui.theme}
|
theme={config.ui.theme}
|
||||||
@@ -347,20 +398,20 @@ class MarkdownEditor extends React.Component {
|
|||||||
sanitize={config.preview.sanitize}
|
sanitize={config.preview.sanitize}
|
||||||
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
|
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={e => this.handleContextMenu(e)}
|
||||||
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
onDoubleClick={e => this.handleDoubleClick(e)}
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
value={this.state.renderValue}
|
value={this.state.renderValue}
|
||||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
onMouseUp={e => this.handlePreviewMouseUp(e)}
|
||||||
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
|
onMouseDown={e => this.handlePreviewMouseDown(e)}
|
||||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
onCheckboxClick={e => this.handleCheckboxClick(e)}
|
||||||
showCopyNotification={config.ui.showCopyNotification}
|
showCopyNotification={config.ui.showCopyNotification}
|
||||||
storagePath={storage.path}
|
storagePath={storage.path}
|
||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
customCSS={config.preview.customCSS}
|
customCSS={config.preview.customCSS}
|
||||||
allowCustomCSS={config.preview.allowCustomCSS}
|
allowCustomCSS={config.preview.allowCustomCSS}
|
||||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||||
onDrop={(e) => this.handleDropImage(e)}
|
onDrop={e => this.handleDropImage(e)}
|
||||||
RTL={RTL}
|
RTL={RTL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import i18n from 'browser/lib/i18n'
|
|||||||
|
|
||||||
const { remote, shell } = require('electron')
|
const { remote, shell } = require('electron')
|
||||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||||
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu
|
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder')
|
||||||
|
.buildMarkdownPreviewContextMenu
|
||||||
|
|
||||||
const { app } = remote
|
const { app } = remote
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
@@ -104,11 +105,14 @@ body {
|
|||||||
font-family: '${fontFamily.join("','")}';
|
font-family: '${fontFamily.join("','")}';
|
||||||
font-size: ${fontSize}px;
|
font-size: ${fontSize}px;
|
||||||
|
|
||||||
${scrollPastEnd ? `
|
${
|
||||||
|
scrollPastEnd
|
||||||
|
? `
|
||||||
padding-bottom: 90vh;
|
padding-bottom: 90vh;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
`
|
`
|
||||||
: ''}
|
: ''
|
||||||
|
}
|
||||||
${RTL ? 'direction: rtl;' : ''}
|
${RTL ? 'direction: rtl;' : ''}
|
||||||
${RTL ? 'text-align: right;' : ''}
|
${RTL ? 'text-align: right;' : ''}
|
||||||
}
|
}
|
||||||
@@ -299,7 +303,11 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV"
|
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.
|
const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
|
||||||
|
|
||||||
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
if (
|
||||||
|
config.editor.switchPreview === 'RIGHTCLICK' &&
|
||||||
|
e.buttons === 2 &&
|
||||||
|
config.editor.type === 'SPLIT'
|
||||||
|
) {
|
||||||
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
||||||
}
|
}
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
@@ -315,7 +323,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e)
|
if (this.props.onMouseDown != null && targetTag === 'BODY')
|
||||||
|
this.props.onMouseDown(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp(e) {
|
handleMouseUp(e) {
|
||||||
@@ -360,10 +369,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
RTL
|
RTL
|
||||||
})
|
})
|
||||||
let body = this.refs.root.contentWindow.document.body.innerHTML
|
let body = this.refs.root.contentWindow.document.body.innerHTML
|
||||||
body = attachmentManagement.fixLocalURLS(
|
body = attachmentManagement.fixLocalURLS(body, this.props.storagePath)
|
||||||
body,
|
|
||||||
this.props.storagePath
|
|
||||||
)
|
|
||||||
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
if (global.process.platform === 'win32') {
|
if (global.process.platform === 'win32') {
|
||||||
@@ -395,13 +401,23 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsHtml() {
|
handleSaveAsHtml() {
|
||||||
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir)))
|
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) =>
|
||||||
|
Promise.resolve(
|
||||||
|
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsPdf() {
|
handleSaveAsPdf() {
|
||||||
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
|
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
|
||||||
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false, javascript: false}})
|
const printout = new remote.BrowserWindow({
|
||||||
printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir))
|
show: false,
|
||||||
|
webPreferences: { webSecurity: false, javascript: false }
|
||||||
|
})
|
||||||
|
printout.loadURL(
|
||||||
|
'data:text/html;charset=UTF-8,' +
|
||||||
|
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
|
||||||
|
)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
printout.webContents.on('did-finish-load', () => {
|
printout.webContents.on('did-finish-load', () => {
|
||||||
printout.webContents.printToPDF({}, (err, data) => {
|
printout.webContents.printToPDF({}, (err, data) => {
|
||||||
@@ -468,15 +484,16 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
*/
|
*/
|
||||||
escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
|
escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
|
||||||
for (let index = 0; index < splitWithCodeTag.length; index++) {
|
for (let index = 0; index < splitWithCodeTag.length; index++) {
|
||||||
const codeTagRequired = (splitWithCodeTag[index] !== '\`\`\`' && index < splitWithCodeTag.length - 1)
|
const codeTagRequired =
|
||||||
|
splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
|
||||||
if (codeTagRequired) {
|
if (codeTagRequired) {
|
||||||
splitWithCodeTag.splice((index + 1), 0, '\`\`\`')
|
splitWithCodeTag.splice(index + 1, 0, '```')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let inCodeTag = false
|
let inCodeTag = false
|
||||||
let result = ''
|
let result = ''
|
||||||
for (let content of splitWithCodeTag) {
|
for (let content of splitWithCodeTag) {
|
||||||
if (content === '\`\`\`') {
|
if (content === '```') {
|
||||||
inCodeTag = !inCodeTag
|
inCodeTag = !inCodeTag
|
||||||
} else if (inCodeTag) {
|
} else if (inCodeTag) {
|
||||||
content = escapeHtmlCharacters(content)
|
content = escapeHtmlCharacters(content)
|
||||||
@@ -489,7 +506,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
getScrollBarStyle() {
|
getScrollBarStyle() {
|
||||||
const { theme } = this.props
|
const { theme } = this.props
|
||||||
|
|
||||||
return uiThemes.some(item => item.name === theme && item.isDark) ? scrollBarDarkStyle : scrollBarStyle
|
return uiThemes.some(item => item.name === theme && item.isDark)
|
||||||
|
? scrollBarDarkStyle
|
||||||
|
: scrollBarStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -542,10 +561,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
'scroll',
|
'scroll',
|
||||||
this.scrollHandler
|
this.scrollHandler
|
||||||
)
|
)
|
||||||
this.refs.root.contentWindow.addEventListener(
|
this.refs.root.contentWindow.addEventListener('resize', this.resizeHandler)
|
||||||
'resize',
|
|
||||||
this.resizeHandler
|
|
||||||
)
|
|
||||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||||
@@ -649,14 +665,15 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
RTL
|
RTL
|
||||||
} = this.props
|
} = this.props
|
||||||
let { fontFamily, codeBlockFontFamily } = this.props
|
let { fontFamily, codeBlockFontFamily } = this.props
|
||||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
fontFamily =
|
||||||
|
_.isString(fontFamily) && fontFamily.trim().length > 0
|
||||||
? fontFamily
|
? fontFamily
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(fontName => fontName.trim())
|
.map(fontName => fontName.trim())
|
||||||
.concat(defaultFontFamily)
|
.concat(defaultFontFamily)
|
||||||
: defaultFontFamily
|
: defaultFontFamily
|
||||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
|
codeBlockFontFamily =
|
||||||
codeBlockFontFamily.trim().length > 0
|
_.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||||
? codeBlockFontFamily
|
? codeBlockFontFamily
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(fontName => fontName.trim())
|
.map(fontName => fontName.trim())
|
||||||
@@ -773,7 +790,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
|
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
|
||||||
|
|
||||||
const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default'
|
const codeBlockThemeClassName = codeBlockTheme
|
||||||
|
? codeBlockTheme.className
|
||||||
|
: 'cm-s-default'
|
||||||
|
|
||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
||||||
@@ -859,7 +878,10 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
el => {
|
el => {
|
||||||
try {
|
try {
|
||||||
const format = el.attributes.getNamedItem('data-format').value
|
const format = el.attributes.getNamedItem('data-format').value
|
||||||
const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
|
const chartConfig =
|
||||||
|
format === 'yaml'
|
||||||
|
? yaml.load(el.innerHTML)
|
||||||
|
: JSON.parse(el.innerHTML)
|
||||||
el.innerHTML = ''
|
el.innerHTML = ''
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
@@ -882,7 +904,12 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
||||||
el => {
|
el => {
|
||||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme, mermaidHTMLLabel)
|
mermaidRender(
|
||||||
|
el,
|
||||||
|
htmlTextHelper.decodeEntities(el.innerHTML),
|
||||||
|
theme,
|
||||||
|
mermaidHTMLLabel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -904,20 +931,14 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
autoplay = 0
|
autoplay = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(<Carousel images={images} autoplay={autoplay} />, el)
|
||||||
<Carousel
|
|
||||||
images={images}
|
|
||||||
autoplay={autoplay}
|
|
||||||
/>,
|
|
||||||
el
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
|
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
|
||||||
const rect = markdownPreviewIframe.getBoundingClientRect()
|
const rect = markdownPreviewIframe.getBoundingClientRect()
|
||||||
const config = { attributes: true, subtree: true }
|
const config = { attributes: true, subtree: true }
|
||||||
const imgObserver = new MutationObserver((mutationList) => {
|
const imgObserver = new MutationObserver(mutationList => {
|
||||||
for (const mu of mutationList) {
|
for (const mu of mutationList) {
|
||||||
if (mu.target.className === 'carouselContent-enter-done') {
|
if (mu.target.className === 'carouselContent-enter-done') {
|
||||||
this.setImgOnClickEventHelper(mu.target, rect)
|
this.setImgOnClickEventHelper(mu.target, rect)
|
||||||
@@ -926,14 +947,18 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
|
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll(
|
||||||
|
'img'
|
||||||
|
)
|
||||||
for (const img of imgList) {
|
for (const img of imgList) {
|
||||||
const parentEl = img.parentElement
|
const parentEl = img.parentElement
|
||||||
this.setImgOnClickEventHelper(img, rect)
|
this.setImgOnClickEventHelper(img, rect)
|
||||||
imgObserver.observe(parentEl, config)
|
imgObserver.observe(parentEl, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('a')
|
const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll(
|
||||||
|
'a'
|
||||||
|
)
|
||||||
for (const a of aList) {
|
for (const a of aList) {
|
||||||
a.removeEventListener('click', this.linkClickHandler)
|
a.removeEventListener('click', this.linkClickHandler)
|
||||||
a.addEventListener('click', this.linkClickHandler)
|
a.addEventListener('click', this.linkClickHandler)
|
||||||
@@ -945,7 +970,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
const widthMagnification = document.body.clientWidth / img.width
|
const widthMagnification = document.body.clientWidth / img.width
|
||||||
const heightMagnification = document.body.clientHeight / img.height
|
const heightMagnification = document.body.clientHeight / img.height
|
||||||
const baseOnWidth = widthMagnification < heightMagnification
|
const baseOnWidth = widthMagnification < heightMagnification
|
||||||
const magnification = baseOnWidth ? widthMagnification : heightMagnification
|
const magnification = baseOnWidth
|
||||||
|
? widthMagnification
|
||||||
|
: heightMagnification
|
||||||
|
|
||||||
const zoomImgWidth = img.width * magnification
|
const zoomImgWidth = img.width * magnification
|
||||||
const zoomImgHeight = img.height * magnification
|
const zoomImgHeight = img.height * magnification
|
||||||
@@ -976,10 +1003,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
width: ${zoomImgWidth};
|
width: ${zoomImgWidth};
|
||||||
height: ${zoomImgHeight}px;
|
height: ${zoomImgHeight}px;
|
||||||
`
|
`
|
||||||
zoomImg.animate([
|
zoomImg.animate([originalImgRect, zoomInImgRect], animationSpeed)
|
||||||
originalImgRect,
|
|
||||||
zoomInImgRect
|
|
||||||
], animationSpeed)
|
|
||||||
|
|
||||||
const overlay = document.createElement('div')
|
const overlay = document.createElement('div')
|
||||||
overlay.style = `
|
overlay.style = `
|
||||||
@@ -1000,10 +1024,10 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
width: ${img.width}px;
|
width: ${img.width}px;
|
||||||
height: ${img.height}px;
|
height: ${img.height}px;
|
||||||
`
|
`
|
||||||
const zoomOutImgAnimation = zoomImg.animate([
|
const zoomOutImgAnimation = zoomImg.animate(
|
||||||
zoomInImgRect,
|
[zoomInImgRect, originalImgRect],
|
||||||
originalImgRect
|
animationSpeed
|
||||||
], animationSpeed)
|
)
|
||||||
zoomOutImgAnimation.onfinish = () => overlay.remove()
|
zoomOutImgAnimation.onfinish = () => overlay.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1095,9 +1119,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
if (posOfHash > -1) {
|
if (posOfHash > -1) {
|
||||||
const extractedId = linkHash.slice(posOfHash + 1)
|
const extractedId = linkHash.slice(posOfHash + 1)
|
||||||
const targetId = mdurl.encode(extractedId)
|
const targetId = mdurl.encode(extractedId)
|
||||||
const targetElement = this.getWindow().document.getElementById(
|
const targetElement = this.getWindow().document.getElementById(targetId)
|
||||||
targetId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (targetElement != null) {
|
if (targetElement != null) {
|
||||||
this.scrollTo(0, targetElement.offsetTop)
|
this.scrollTo(0, targetElement.offsetTop)
|
||||||
@@ -1140,7 +1162,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
openExternal(href) {
|
openExternal(href) {
|
||||||
try {
|
try {
|
||||||
const success = shell.openExternal(href) || shell.openExternal(decodeURI(href))
|
const success =
|
||||||
|
shell.openExternal(href) || shell.openExternal(decodeURI(href))
|
||||||
if (!success) console.error('failed to open url ' + href)
|
if (!success) console.error('failed to open url ' + href)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// URI Error threw from decodeURI
|
// URI Error threw from decodeURI
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
handleScroll(e) {
|
handleScroll(e) {
|
||||||
if (!this.props.config.preview.scrollSync) return
|
if (!this.props.config.preview.scrollSync) return
|
||||||
|
|
||||||
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
const previewDoc = _.get(
|
||||||
|
this,
|
||||||
|
'refs.preview.refs.root.contentWindow.document'
|
||||||
|
)
|
||||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||||
let srcTop, srcHeight, targetTop, targetHeight
|
let srcTop, srcHeight, targetTop, targetHeight
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
targetHeight = _.get(codeDoc, 'height')
|
targetHeight = _.get(codeDoc, 'height')
|
||||||
}
|
}
|
||||||
|
|
||||||
const distance = (targetHeight * srcTop / srcHeight) - targetTop
|
const distance = (targetHeight * srcTop) / srcHeight - targetTop
|
||||||
const framerate = 1000 / 60
|
const framerate = 1000 / 60
|
||||||
const frames = 20
|
const frames = 20
|
||||||
const refractory = frames * framerate
|
const refractory = frames * framerate
|
||||||
@@ -60,14 +63,22 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
let scrollPos, time
|
let scrollPos, time
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
time = frame / frames
|
time = frame / frames
|
||||||
scrollPos = time < 0.5
|
scrollPos =
|
||||||
|
time < 0.5
|
||||||
? 2 * time * time // ease in
|
? 2 * time * time // ease in
|
||||||
: -1 + (4 - 2 * time) * time // ease out
|
: -1 + (4 - 2 * time) * time // ease out
|
||||||
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
|
if (e.doc)
|
||||||
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance)
|
_.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
|
||||||
|
else
|
||||||
|
_.get(this, 'refs.code.editor').scrollTo(
|
||||||
|
0,
|
||||||
|
targetTop + scrollPos * distance
|
||||||
|
)
|
||||||
if (frame >= frames) {
|
if (frame >= frames) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
setTimeout(() => { this.userScroll = true }, refractory)
|
setTimeout(() => {
|
||||||
|
this.userScroll = true
|
||||||
|
}, refractory)
|
||||||
}
|
}
|
||||||
frame++
|
frame++
|
||||||
}, framerate)
|
}, framerate)
|
||||||
@@ -83,9 +94,9 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
const checkReplace = /\[x]/i
|
const checkReplace = /\[x]/i
|
||||||
const uncheckReplace = /\[ ]/
|
const uncheckReplace = /\[ ]/
|
||||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
const lineIndex =
|
||||||
const lines = this.refs.code.value
|
parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
.split('\n')
|
const lines = this.refs.code.value.split('\n')
|
||||||
|
|
||||||
const targetLine = lines[lineIndex]
|
const targetLine = lines[lineIndex]
|
||||||
let newLine = targetLine
|
let newLine = targetLine
|
||||||
@@ -105,7 +116,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
const rootRect = this.refs.root.getBoundingClientRect()
|
const rootRect = this.refs.root.getBoundingClientRect()
|
||||||
const rootWidth = rootRect.width
|
const rootWidth = rootRect.width
|
||||||
const offset = rootRect.left
|
const offset = rootRect.left
|
||||||
let newCodeEditorWidthInPercent = (e.pageX - offset) / rootWidth * 100
|
let newCodeEditorWidthInPercent = ((e.pageX - offset) / rootWidth) * 100
|
||||||
|
|
||||||
// limit minSize to 10%, maxSize to 90%
|
// limit minSize to 10%, maxSize to 90%
|
||||||
if (newCodeEditorWidthInPercent <= 10) {
|
if (newCodeEditorWidthInPercent <= 10) {
|
||||||
@@ -137,19 +148,30 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {config, value, storageKey, noteKey, linesHighlighted, RTL} = this.props
|
const {
|
||||||
|
config,
|
||||||
|
value,
|
||||||
|
storageKey,
|
||||||
|
noteKey,
|
||||||
|
linesHighlighted,
|
||||||
|
RTL
|
||||||
|
} = this.props
|
||||||
const storage = findStorage(storageKey)
|
const storage = findStorage(storageKey)
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
const previewStyle = {}
|
const previewStyle = {}
|
||||||
previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%'
|
previewStyle.width = 100 - this.state.codeEditorWidthInPercent + '%'
|
||||||
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
|
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused)
|
||||||
|
previewStyle.pointerEvents = 'none'
|
||||||
return (
|
return (
|
||||||
<div styleName='root' ref='root'
|
<div
|
||||||
|
styleName='root'
|
||||||
|
ref='root'
|
||||||
onMouseMove={e => this.handleMouseMove(e)}
|
onMouseMove={e => this.handleMouseMove(e)}
|
||||||
onMouseUp={e => this.handleMouseUp(e)}>
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
|
>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
ref='code'
|
ref='code'
|
||||||
width={this.state.codeEditorWidthInPercent + '%'}
|
width={this.state.codeEditorWidthInPercent + '%'}
|
||||||
@@ -174,7 +196,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
linesHighlighted={linesHighlighted}
|
linesHighlighted={linesHighlighted}
|
||||||
onChange={(e) => this.handleOnChange(e)}
|
onChange={e => this.handleOnChange(e)}
|
||||||
onScroll={this.handleScroll.bind(this)}
|
onScroll={this.handleScroll.bind(this)}
|
||||||
spellCheck={config.editor.spellcheck}
|
spellCheck={config.editor.spellcheck}
|
||||||
enableSmartPaste={config.editor.enableSmartPaste}
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
@@ -185,7 +207,11 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||||
RTL={RTL}
|
RTL={RTL}
|
||||||
/>
|
/>
|
||||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
<div
|
||||||
|
styleName='slider'
|
||||||
|
style={{ left: this.state.codeEditorWidthInPercent + '%' }}
|
||||||
|
onMouseDown={e => this.handleMouseDown(e)}
|
||||||
|
>
|
||||||
<div styleName='slider-hitbox' />
|
<div styleName='slider-hitbox' />
|
||||||
</div>
|
</div>
|
||||||
<MarkdownPreview
|
<MarkdownPreview
|
||||||
@@ -206,7 +232,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
ref='preview'
|
ref='preview'
|
||||||
tabInde='0'
|
tabInde='0'
|
||||||
value={value}
|
value={value}
|
||||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
onCheckboxClick={e => this.handleCheckboxClick(e)}
|
||||||
onScroll={this.handleScroll.bind(this)}
|
onScroll={this.handleScroll.bind(this)}
|
||||||
showCopyNotification={config.ui.showCopyNotification}
|
showCopyNotification={config.ui.showCopyNotification}
|
||||||
storagePath={storage.path}
|
storagePath={storage.path}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './ModalEscButton.styl'
|
import styles from './ModalEscButton.styl'
|
||||||
|
|
||||||
const ModalEscButton = ({
|
const ModalEscButton = ({ handleEscButtonClick }) => (
|
||||||
handleEscButtonClick
|
|
||||||
}) => (
|
|
||||||
<button styleName='escButton' onClick={handleEscButtonClick}>
|
<button styleName='escButton' onClick={handleEscButtonClick}>
|
||||||
<div styleName='esc-mark'>×</div>
|
<div styleName='esc-mark'>×</div>
|
||||||
<div>esc</div>
|
<div>esc</div>
|
||||||
|
|||||||
@@ -12,13 +12,12 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const NavToggleButton = ({ isFolded, handleToggleButtonClick }) => (
|
const NavToggleButton = ({ isFolded, handleToggleButtonClick }) => (
|
||||||
<button styleName='navToggle'
|
<button styleName='navToggle' onClick={e => handleToggleButtonClick(e)}>
|
||||||
onClick={(e) => handleToggleButtonClick(e)}
|
{isFolded ? (
|
||||||
>
|
<i className='fa fa-angle-double-right fa-2x' />
|
||||||
{isFolded
|
) : (
|
||||||
? <i className='fa fa-angle-double-right fa-2x' />
|
<i className='fa fa-angle-double-left fa-2x' />
|
||||||
: <i className='fa fa-angle-double-left fa-2x' />
|
)}
|
||||||
}
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ const TagElement = ({ tagName, color }) => {
|
|||||||
const style = {}
|
const style = {}
|
||||||
if (color) {
|
if (color) {
|
||||||
style.backgroundColor = color
|
style.backgroundColor = color
|
||||||
style.color = invertColor(color, { black: '#222', white: '#f1f1f1', threshold: 0.3 })
|
style.color = invertColor(color, {
|
||||||
|
black: '#222',
|
||||||
|
white: '#f1f1f1',
|
||||||
|
threshold: 0.3
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span styleName='item-bottom-tagList-item' key={tagName} style={style}>
|
<span styleName='item-bottom-tagList-item' key={tagName} style={style}>
|
||||||
@@ -44,9 +48,13 @@ const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showTagsAlphabetically) {
|
if (showTagsAlphabetically) {
|
||||||
return sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
return sortBy(tags).map(tag =>
|
||||||
|
TagElement({ tagName: tag, color: coloredTags[tag] })
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
return tags.map(tag =>
|
||||||
|
TagElement({ tagName: tag, color: coloredTags[tag] })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,13 +91,17 @@ const NoteItem = ({
|
|||||||
draggable='true'
|
draggable='true'
|
||||||
>
|
>
|
||||||
<div styleName='item-wrapper'>
|
<div styleName='item-wrapper'>
|
||||||
{note.type === 'SNIPPET_NOTE'
|
{note.type === 'SNIPPET_NOTE' ? (
|
||||||
? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
|
<i styleName='item-title-icon' className='fa fa-fw fa-code' />
|
||||||
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
|
) : (
|
||||||
|
<i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />
|
||||||
|
)}
|
||||||
<div styleName='item-title'>
|
<div styleName='item-title'>
|
||||||
{note.title.trim().length > 0
|
{note.title.trim().length > 0 ? (
|
||||||
? <Emoji text={note.title} />
|
<Emoji text={note.title} />
|
||||||
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
|
) : (
|
||||||
|
<span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div styleName='item-middle'>
|
<div styleName='item-middle'>
|
||||||
<div styleName='item-middle-time'>{dateDisplay}</div>
|
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||||
@@ -98,7 +110,9 @@ const NoteItem = ({
|
|||||||
title={
|
title={
|
||||||
viewType === 'ALL'
|
viewType === 'ALL'
|
||||||
? storageName
|
? storageName
|
||||||
: viewType === 'STORAGE' ? folderName : null
|
: viewType === 'STORAGE'
|
||||||
|
? folderName
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
styleName='item-middle-app-meta-label'
|
styleName='item-middle-app-meta-label'
|
||||||
>
|
>
|
||||||
@@ -109,28 +123,36 @@ const NoteItem = ({
|
|||||||
</div>
|
</div>
|
||||||
<div styleName='item-bottom'>
|
<div styleName='item-bottom'>
|
||||||
<div styleName='item-bottom-tagList'>
|
<div styleName='item-bottom-tagList'>
|
||||||
{note.tags.length > 0
|
{note.tags.length > 0 ? (
|
||||||
? TagElementList(note.tags, showTagsAlphabetically, coloredTags)
|
TagElementList(note.tags, showTagsAlphabetically, coloredTags)
|
||||||
: <span
|
) : (
|
||||||
|
<span
|
||||||
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
||||||
styleName='item-bottom-tagList-empty'
|
styleName='item-bottom-tagList-empty'
|
||||||
>
|
>
|
||||||
{i18n.__('No tags')}
|
{i18n.__('No tags')}
|
||||||
</span>}
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{note.isStarred
|
{note.isStarred ? (
|
||||||
? <img
|
<img
|
||||||
styleName='item-star'
|
styleName='item-star'
|
||||||
src='../resources/icon/icon-starred.svg'
|
src='../resources/icon/icon-starred.svg'
|
||||||
/>
|
/>
|
||||||
: ''}
|
) : (
|
||||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
''
|
||||||
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
)}
|
||||||
: ''}
|
{note.isPinned && !pathname.match(/\/starred|\/trash/) ? (
|
||||||
{note.type === 'MARKDOWN_NOTE'
|
<i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
) : (
|
||||||
: ''}
|
''
|
||||||
|
)}
|
||||||
|
{note.type === 'MARKDOWN_NOTE' ? (
|
||||||
|
<TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,10 +25,8 @@ const NoteItemSimple = ({
|
|||||||
pathname,
|
pathname,
|
||||||
storage
|
storage
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName={isActive
|
<div
|
||||||
? 'item-simple--active'
|
styleName={isActive ? 'item-simple--active' : 'item-simple'}
|
||||||
: 'item-simple'
|
|
||||||
}
|
|
||||||
key={note.key}
|
key={note.key}
|
||||||
onClick={e => handleNoteClick(e, note.key)}
|
onClick={e => handleNoteClick(e, note.key)}
|
||||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||||
@@ -36,23 +34,29 @@ const NoteItemSimple = ({
|
|||||||
draggable='true'
|
draggable='true'
|
||||||
>
|
>
|
||||||
<div styleName='item-simple-title'>
|
<div styleName='item-simple-title'>
|
||||||
{note.type === 'SNIPPET_NOTE'
|
{note.type === 'SNIPPET_NOTE' ? (
|
||||||
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
|
<i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
|
||||||
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
) : (
|
||||||
}
|
<i
|
||||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
styleName='item-simple-title-icon'
|
||||||
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
className='fa fa-fw fa-file-text-o'
|
||||||
: ''
|
/>
|
||||||
}
|
)}
|
||||||
{note.title.trim().length > 0
|
{note.isPinned && !pathname.match(/\/starred|\/trash/) ? (
|
||||||
? note.title
|
<i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||||
: <span styleName='item-simple-title-empty'>{i18n.__('Empty note')}</span>
|
) : (
|
||||||
}
|
''
|
||||||
{isAllNotesView && <div styleName='item-simple-right'>
|
)}
|
||||||
<span styleName='item-simple-right-storageName'>
|
{note.title.trim().length > 0 ? (
|
||||||
{storage.name}
|
note.title
|
||||||
</span>
|
) : (
|
||||||
</div>}
|
<span styleName='item-simple-title-empty'>{i18n.__('Empty note')}</span>
|
||||||
|
)}
|
||||||
|
{isAllNotesView && (
|
||||||
|
<div styleName='item-simple-right'>
|
||||||
|
<span styleName='item-simple-right-storageName'>{storage.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class RealtimeNotification extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchNotifications() {
|
fetchNotifications() {
|
||||||
const notificationsUrl = 'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
|
const notificationsUrl =
|
||||||
|
'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
|
||||||
fetch(notificationsUrl)
|
fetch(notificationsUrl)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.json()
|
return response.json()
|
||||||
@@ -36,16 +37,23 @@ class RealtimeNotification extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { notifications } = this.state
|
const { notifications } = this.state
|
||||||
const link = notifications.length > 0
|
const link =
|
||||||
? <a styleName='notification-link' href={notifications[0].linkUrl}
|
notifications.length > 0 ? (
|
||||||
onClick={(e) => this.handleLinkClick(e)}
|
<a
|
||||||
|
styleName='notification-link'
|
||||||
|
href={notifications[0].linkUrl}
|
||||||
|
onClick={e => this.handleLinkClick(e)}
|
||||||
>
|
>
|
||||||
Info: {notifications[0].text}
|
Info: {notifications[0].text}
|
||||||
</a>
|
</a>
|
||||||
: ''
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div styleName='notification-area' style={this.props.style}>{link}</div>
|
<div styleName='notification-area' style={this.props.style}>
|
||||||
|
{link}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,27 @@ import i18n from 'browser/lib/i18n'
|
|||||||
* @return {React.Component}
|
* @return {React.Component}
|
||||||
*/
|
*/
|
||||||
const SideNavFilter = ({
|
const SideNavFilter = ({
|
||||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
isFolded,
|
||||||
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
isHomeActive,
|
||||||
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
|
handleAllNotesButtonClick,
|
||||||
|
isStarredActive,
|
||||||
|
handleStarredButtonClick,
|
||||||
|
isTrashedActive,
|
||||||
|
handleTrashedButtonClick,
|
||||||
|
counterDelNote,
|
||||||
|
counterTotalNote,
|
||||||
|
counterStarredNote,
|
||||||
|
handleFilterButtonContextMenu
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||||
|
<button
|
||||||
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
|
styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
|
||||||
onClick={handleAllNotesButtonClick}
|
onClick={handleAllNotesButtonClick}
|
||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isHomeActive
|
<img
|
||||||
|
src={
|
||||||
|
isHomeActive
|
||||||
? '../resources/icon/icon-all-active.svg'
|
? '../resources/icon/icon-all-active.svg'
|
||||||
: '../resources/icon/icon-all.svg'
|
: '../resources/icon/icon-all.svg'
|
||||||
}
|
}
|
||||||
@@ -36,11 +46,14 @@ const SideNavFilter = ({
|
|||||||
<span styleName='counters'>{counterTotalNote}</span>
|
<span styleName='counters'>{counterTotalNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'}
|
<button
|
||||||
|
styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'}
|
||||||
onClick={handleStarredButtonClick}
|
onClick={handleStarredButtonClick}
|
||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isStarredActive
|
<img
|
||||||
|
src={
|
||||||
|
isStarredActive
|
||||||
? '../resources/icon/icon-star-active.svg'
|
? '../resources/icon/icon-star-active.svg'
|
||||||
: '../resources/icon/icon-star-sidenav.svg'
|
: '../resources/icon/icon-star-sidenav.svg'
|
||||||
}
|
}
|
||||||
@@ -50,11 +63,15 @@ const SideNavFilter = ({
|
|||||||
<span styleName='counters'>{counterStarredNote}</span>
|
<span styleName='counters'>{counterStarredNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
|
<button
|
||||||
onClick={handleTrashedButtonClick} onContextMenu={handleFilterButtonContextMenu}
|
styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
|
||||||
|
onClick={handleTrashedButtonClick}
|
||||||
|
onContextMenu={handleFilterButtonContextMenu}
|
||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isTrashedActive
|
<img
|
||||||
|
src={
|
||||||
|
isTrashedActive
|
||||||
? '../resources/icon/icon-trash-active.svg'
|
? '../resources/icon/icon-trash-active.svg'
|
||||||
: '../resources/icon/icon-trash-sidenav.svg'
|
: '../resources/icon/icon-trash-sidenav.svg'
|
||||||
}
|
}
|
||||||
@@ -63,7 +80,6 @@ const SideNavFilter = ({
|
|||||||
<span styleName='menu-button-label'>{i18n.__('Trash')}</span>
|
<span styleName='menu-button-label'>{i18n.__('Trash')}</span>
|
||||||
<span styleName='counters'>{counterDelNote}</span>
|
<span styleName='counters'>{counterDelNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class SnippetTab extends React.Component {
|
|||||||
context.popup([
|
context.popup([
|
||||||
{
|
{
|
||||||
label: i18n.__('Rename'),
|
label: i18n.__('Rename'),
|
||||||
click: (e) => this.handleRenameClick(e)
|
click: e => this.handleRenameClick(e)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -64,13 +64,16 @@ class SnippetTab extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleRename() {
|
handleRename() {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
isRenaming: false
|
isRenaming: false
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
if (this.props.snippet.name !== this.state.name) {
|
if (this.props.snippet.name !== this.state.name) {
|
||||||
this.props.onRename(this.state.name)
|
this.props.onRename(this.state.name)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteButtonClick(e) {
|
handleDeleteButtonClick(e) {
|
||||||
@@ -78,12 +81,15 @@ class SnippetTab extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startRenaming() {
|
startRenaming() {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
isRenaming: true
|
isRenaming: true
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.name.focus()
|
this.refs.name.focus()
|
||||||
this.refs.name.select()
|
this.refs.name.select()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragStart(e) {
|
handleDragStart(e) {
|
||||||
@@ -98,49 +104,46 @@ class SnippetTab extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { isActive, snippet, isDeletable } = this.props
|
const { isActive, snippet, isDeletable } = this.props
|
||||||
return (
|
return (
|
||||||
<div styleName={isActive
|
<div styleName={isActive ? 'root--active' : 'root'}>
|
||||||
? 'root--active'
|
{!this.state.isRenaming ? (
|
||||||
: 'root'
|
<button
|
||||||
}
|
styleName='button'
|
||||||
>
|
onClick={e => this.handleClick(e)}
|
||||||
{!this.state.isRenaming
|
onDoubleClick={e => this.handleRenameClick(e)}
|
||||||
? <button styleName='button'
|
onContextMenu={e => this.handleContextMenu(e)}
|
||||||
onClick={(e) => this.handleClick(e)}
|
onDragStart={e => this.handleDragStart(e)}
|
||||||
onDoubleClick={(e) => this.handleRenameClick(e)}
|
onDrop={e => this.handleDrop(e)}
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
|
||||||
onDragStart={(e) => this.handleDragStart(e)}
|
|
||||||
onDrop={(e) => this.handleDrop(e)}
|
|
||||||
draggable='true'
|
draggable='true'
|
||||||
>
|
>
|
||||||
{snippet.name.trim().length > 0
|
{snippet.name.trim().length > 0 ? (
|
||||||
? snippet.name
|
snippet.name
|
||||||
: <span>
|
) : (
|
||||||
{i18n.__('Unnamed')}
|
<span>{i18n.__('Unnamed')}</span>
|
||||||
</span>
|
)}
|
||||||
}
|
|
||||||
</button>
|
</button>
|
||||||
: <input styleName='input'
|
) : (
|
||||||
|
<input
|
||||||
|
styleName='input'
|
||||||
ref='name'
|
ref='name'
|
||||||
value={this.state.name}
|
value={this.state.name}
|
||||||
onChange={(e) => this.handleNameInputChange(e)}
|
onChange={e => this.handleNameInputChange(e)}
|
||||||
onBlur={(e) => this.handleNameInputBlur(e)}
|
onBlur={e => this.handleNameInputBlur(e)}
|
||||||
onKeyDown={(e) => this.handleNameInputKeyDown(e)}
|
onKeyDown={e => this.handleNameInputKeyDown(e)}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{isDeletable &&
|
{isDeletable && (
|
||||||
<button styleName='deleteButton'
|
<button
|
||||||
onClick={(e) => this.handleDeleteButtonClick(e)}
|
styleName='deleteButton'
|
||||||
|
onClick={e => this.handleDeleteButtonClick(e)}
|
||||||
>
|
>
|
||||||
<i className='fa fa-times' />
|
<i className='fa fa-times' />
|
||||||
</button>
|
</button>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SnippetTab.propTypes = {
|
SnippetTab.propTypes = {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CSSModules(SnippetTab, styles)
|
export default CSSModules(SnippetTab, styles)
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ const StorageItem = ({
|
|||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
>
|
>
|
||||||
{!isFolded &&
|
{!isFolded && (
|
||||||
<DraggableIcon className={styles['folderList-item-reorder']} />}
|
<DraggableIcon className={styles['folderList-item-reorder']} />
|
||||||
|
)}
|
||||||
<span
|
<span
|
||||||
styleName={
|
styleName={
|
||||||
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||||
@@ -70,11 +71,12 @@ const StorageItem = ({
|
|||||||
? _.truncate(folderName, { length: 1, omission: '' })
|
? _.truncate(folderName, { length: 1, omission: '' })
|
||||||
: folderName}
|
: folderName}
|
||||||
</span>
|
</span>
|
||||||
{!isFolded &&
|
{!isFolded && _.isNumber(noteCount) && (
|
||||||
_.isNumber(noteCount) &&
|
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>}
|
)}
|
||||||
{isFolded &&
|
{isFolded && (
|
||||||
<span styleName='folderList-item-tooltip'>{folderName}</span>}
|
<span styleName='folderList-item-tooltip'>{folderName}</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
|
|
||||||
const StorageList = ({ storageList, isFolded }) => (
|
const StorageList = ({ storageList, isFolded }) => (
|
||||||
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
|
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
|
||||||
{storageList.length > 0 ? storageList : (
|
{storageList.length > 0 ? (
|
||||||
|
storageList
|
||||||
|
) : (
|
||||||
<div styleName='storageList-empty'>No storage mount.</div>
|
<div styleName='storageList-empty'>No storage mount.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,16 +15,44 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
* @param {string} bgColor tab backgroundColor
|
* @param {string} bgColor tab backgroundColor
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count, color}) => (
|
const TagListItem = ({
|
||||||
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}>
|
name,
|
||||||
{isRelated
|
handleClickTagListItem,
|
||||||
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
handleClickNarrowToTag,
|
||||||
|
handleContextMenu,
|
||||||
|
isActive,
|
||||||
|
isRelated,
|
||||||
|
count,
|
||||||
|
color
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
styleName='tagList-itemContainer'
|
||||||
|
onContextMenu={e => handleContextMenu(e, name)}
|
||||||
|
>
|
||||||
|
{isRelated ? (
|
||||||
|
<button
|
||||||
|
styleName={
|
||||||
|
isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'
|
||||||
|
}
|
||||||
|
onClick={() => handleClickNarrowToTag(name)}
|
||||||
|
>
|
||||||
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
|
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
|
||||||
</button>
|
</button>
|
||||||
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
) : (
|
||||||
|
<div
|
||||||
|
styleName={
|
||||||
|
isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'
|
||||||
}
|
}
|
||||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
/>
|
||||||
<span styleName='tagList-item-color' style={{backgroundColor: color || 'transparent'}} />
|
)}
|
||||||
|
<button
|
||||||
|
styleName={isActive ? 'tagList-item-active' : 'tagList-item'}
|
||||||
|
onClick={() => handleClickTagListItem(name)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
styleName='tagList-item-color'
|
||||||
|
style={{ backgroundColor: color || 'transparent' }}
|
||||||
|
/>
|
||||||
<span styleName='tagList-item-name'>
|
<span styleName='tagList-item-name'>
|
||||||
{`# ${name}`}
|
{`# ${name}`}
|
||||||
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
|
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
|
||||||
|
|||||||
@@ -11,17 +11,20 @@ import styles from './TodoListPercentage.styl'
|
|||||||
* @param {number} percentageOfTodo
|
* @param {number} percentageOfTodo
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TodoListPercentage = ({
|
const TodoListPercentage = ({ percentageOfTodo, onClearCheckboxClick }) => (
|
||||||
percentageOfTodo, onClearCheckboxClick
|
<div
|
||||||
}) => (
|
styleName='percentageBar'
|
||||||
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
style={{ display: isNaN(percentageOfTodo) ? 'none' : '' }}
|
||||||
|
>
|
||||||
<div styleName='progressBar' style={{ width: `${percentageOfTodo}%` }}>
|
<div styleName='progressBar' style={{ width: `${percentageOfTodo}%` }}>
|
||||||
<div styleName='progressBarInner'>
|
<div styleName='progressBarInner'>
|
||||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='todoClear'>
|
<div styleName='todoClear'>
|
||||||
<p styleName='todoClearText' onClick={(e) => onClearCheckboxClick(e)}>clear</p>
|
<p styleName='todoClearText' onClick={e => onClearCheckboxClick(e)}>
|
||||||
|
clear
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,18 +8,21 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './TodoProcess.styl'
|
import styles from './TodoProcess.styl'
|
||||||
|
|
||||||
const TodoProcess = ({
|
const TodoProcess = ({
|
||||||
todoStatus: {
|
todoStatus: { total: totalTodo, completed: completedTodo }
|
||||||
total: totalTodo,
|
|
||||||
completed: completedTodo
|
|
||||||
}
|
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName='todo-process' style={{display: totalTodo > 0 ? '' : 'none'}}>
|
<div
|
||||||
|
styleName='todo-process'
|
||||||
|
style={{ display: totalTodo > 0 ? '' : 'none' }}
|
||||||
|
>
|
||||||
<div styleName='todo-process-text'>
|
<div styleName='todo-process-text'>
|
||||||
<i className='fa fa-fw fa-check-square-o' />
|
<i className='fa fa-fw fa-check-square-o' />
|
||||||
{completedTodo} of {totalTodo}
|
{completedTodo} of {totalTodo}
|
||||||
</div>
|
</div>
|
||||||
<div styleName='todo-process-bar'>
|
<div styleName='todo-process-bar'>
|
||||||
<div styleName='todo-process-bar--inner' style={{width: parseInt(completedTodo / totalTodo * 100) + '%'}} />
|
<div
|
||||||
|
styleName='todo-process-bar--inner'
|
||||||
|
style={{ width: parseInt((completedTodo / totalTodo) * 100) + '%' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ function render (element, content, theme, enableHTMLLabel) {
|
|||||||
element.style.height = height.value + 'vh'
|
element.style.height = height.value + 'vh'
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDarkTheme = uiThemes.some(item => item.name === theme && item.isDark)
|
const isDarkTheme = uiThemes.some(
|
||||||
|
item => item.name === theme && item.isDark
|
||||||
|
)
|
||||||
|
|
||||||
mermaidAPI.initialize({
|
mermaidAPI.initialize({
|
||||||
theme: isDarkTheme ? 'dark' : 'default',
|
theme: isDarkTheme ? 'dark' : 'default',
|
||||||
@@ -42,7 +44,7 @@ function render (element, content, theme, enableHTMLLabel) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mermaidAPI.render(getId(), content, (svgGraph) => {
|
mermaidAPI.render(getId(), content, svgGraph => {
|
||||||
element.innerHTML = svgGraph
|
element.innerHTML = svgGraph
|
||||||
|
|
||||||
if (!isPredefined) {
|
if (!isPredefined) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function parse (boostnotercPath = _boostnotercPath) {
|
|||||||
return JSON.parse(sander.readFileSync(boostnotercPath).toString())
|
return JSON.parse(sander.readFileSync(boostnotercPath).toString())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
console.warn('Your .boostnoterc is broken so it\'s not used.')
|
console.warn("Your .boostnoterc is broken so it's not used.")
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ class SnippetManager {
|
|||||||
id: crypto.randomBytes(16).toString('hex'),
|
id: crypto.randomBytes(16).toString('hex'),
|
||||||
name: 'Dummy text',
|
name: 'Dummy text',
|
||||||
prefix: ['lorem', 'ipsum'],
|
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.'
|
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.snippets = []
|
||||||
|
|||||||
@@ -76,11 +76,7 @@ export default class TextEditorInterface {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.doc.replaceRange(
|
this.doc.replaceRange('', { line: row, ch: 0 }, { line: row + 1, ch: 0 })
|
||||||
'',
|
|
||||||
{ line: row, ch: 0 },
|
|
||||||
{ line: row + 1, ch: 0 }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export function confirmDeleteNote (confirmDeletion, permanent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dialogButtonIndex = dialog.showMessageBox(
|
const dialogButtonIndex = dialog.showMessageBox(
|
||||||
remote.getCurrentWindow(), alertConfig
|
remote.getCurrentWindow(),
|
||||||
|
alertConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
return dialogButtonIndex === 0
|
return dialogButtonIndex === 0
|
||||||
|
|||||||
@@ -9,12 +9,17 @@ const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme'
|
|||||||
const isProduction = process.env.NODE_ENV === 'production'
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
const paths = [
|
const paths = [
|
||||||
isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH),
|
isProduction
|
||||||
isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH)
|
? 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
|
const themes = paths
|
||||||
.map(directory => fs.readdirSync(directory).map(file => {
|
.map(directory =>
|
||||||
|
fs.readdirSync(directory).map(file => {
|
||||||
const name = file.substring(0, file.lastIndexOf('.'))
|
const name = file.substring(0, file.lastIndexOf('.'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -22,26 +27,33 @@ const themes = paths
|
|||||||
path: path.join(directory, file),
|
path: path.join(directory, file),
|
||||||
className: `cm-s-${name}`
|
className: `cm-s-${name}`
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
.reduce((accumulator, value) => accumulator.concat(value), [])
|
.reduce((accumulator, value) => accumulator.concat(value), [])
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, {
|
themes.splice(
|
||||||
|
themes.findIndex(({ name }) => name === 'solarized'),
|
||||||
|
1,
|
||||||
|
{
|
||||||
name: 'solarized dark',
|
name: 'solarized dark',
|
||||||
path: path.join(paths[0], 'solarized.css'),
|
path: path.join(paths[0], 'solarized.css'),
|
||||||
className: `cm-s-solarized cm-s-dark`
|
className: `cm-s-solarized cm-s-dark`
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
name: 'solarized light',
|
name: 'solarized light',
|
||||||
path: path.join(paths[0], 'solarized.css'),
|
path: path.join(paths[0], 'solarized.css'),
|
||||||
className: `cm-s-solarized cm-s-light`
|
className: `cm-s-solarized cm-s-light`
|
||||||
})
|
}
|
||||||
|
)
|
||||||
themes.splice(0, 0, {
|
themes.splice(0, 0, {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
path: path.join(paths[0], 'elegant.css'),
|
path: path.join(paths[0], 'elegant.css'),
|
||||||
className: `cm-s-default`
|
className: `cm-s-default`
|
||||||
})
|
})
|
||||||
|
|
||||||
const snippetFile = process.env.NODE_ENV !== 'test'
|
const snippetFile =
|
||||||
|
process.env.NODE_ENV !== 'test'
|
||||||
? path.join(app.getPath('userData'), 'snippets.json')
|
? path.join(app.getPath('userData'), 'snippets.json')
|
||||||
: '' // return nothing as we specified different path to snippets.json in test
|
: '' // return nothing as we specified different path to snippets.json in test
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const { Menu, MenuItem } = remote
|
|||||||
|
|
||||||
function popup(templates) {
|
function popup(templates) {
|
||||||
const menu = new Menu()
|
const menu = new Menu()
|
||||||
templates.forEach((item) => {
|
templates.forEach(item => {
|
||||||
menu.append(new MenuItem(item))
|
menu.append(new MenuItem(item))
|
||||||
})
|
})
|
||||||
menu.popup(remote.getCurrentWindow())
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ const uri2path = require('file-uri-to-path')
|
|||||||
* @returns {Electron.Menu} The created electron context menu
|
* @returns {Electron.Menu} The created electron context menu
|
||||||
*/
|
*/
|
||||||
const buildEditorContextMenu = function(editor, event) {
|
const buildEditorContextMenu = function(editor, event) {
|
||||||
if (editor == null || event == null || event.pageX == null || event.pageY == null) {
|
if (
|
||||||
|
editor == null ||
|
||||||
|
event == null ||
|
||||||
|
event.pageX == null ||
|
||||||
|
event.pageY == null
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const cursor = editor.coordsChar({ left: event.pageX, top: event.pageY })
|
const cursor = editor.coordsChar({ left: event.pageX, top: event.pageY })
|
||||||
@@ -40,30 +45,44 @@ const buildEditorContextMenu = function (editor, event) {
|
|||||||
isMisspelled: isMisspelled,
|
isMisspelled: isMisspelled,
|
||||||
spellingSuggestions: suggestion
|
spellingSuggestions: suggestion
|
||||||
}
|
}
|
||||||
const template = [{
|
const template = [
|
||||||
|
{
|
||||||
role: 'cut'
|
role: 'cut'
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
role: 'copy'
|
role: 'copy'
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
role: 'paste'
|
role: 'paste'
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
|
||||||
if (selection.isMisspelled) {
|
if (selection.isMisspelled) {
|
||||||
const suggestions = selection.spellingSuggestions
|
const suggestions = selection.spellingSuggestions
|
||||||
template.unshift.apply(template, suggestions.map(function (suggestion) {
|
template.unshift.apply(
|
||||||
|
template,
|
||||||
|
suggestions
|
||||||
|
.map(function(suggestion) {
|
||||||
return {
|
return {
|
||||||
label: suggestion,
|
label: suggestion,
|
||||||
click: function(suggestion) {
|
click: function(suggestion) {
|
||||||
if (editor != null) {
|
if (editor != null) {
|
||||||
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head)
|
editor.replaceRange(
|
||||||
|
suggestion.label,
|
||||||
|
wordRange.anchor,
|
||||||
|
wordRange.head
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).concat({
|
})
|
||||||
|
.concat({
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return Menu.buildFromTemplate(template)
|
return Menu.buildFromTemplate(template)
|
||||||
}
|
}
|
||||||
@@ -75,18 +94,29 @@ const buildEditorContextMenu = function (editor, event) {
|
|||||||
* @returns {Electron.Menu} The created electron context menu
|
* @returns {Electron.Menu} The created electron context menu
|
||||||
*/
|
*/
|
||||||
const buildMarkdownPreviewContextMenu = function(markdownPreview, event) {
|
const buildMarkdownPreviewContextMenu = function(markdownPreview, event) {
|
||||||
if (markdownPreview == null || event == null || event.pageX == null || event.pageY == null) {
|
if (
|
||||||
|
markdownPreview == null ||
|
||||||
|
event == null ||
|
||||||
|
event.pageX == null ||
|
||||||
|
event.pageY == null
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default context menu inclusions
|
// Default context menu inclusions
|
||||||
const template = [{
|
const template = [
|
||||||
|
{
|
||||||
role: 'copy'
|
role: 'copy'
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
|
||||||
if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) {
|
if (
|
||||||
|
event.target.tagName.toLowerCase() === 'a' &&
|
||||||
|
event.target.getAttribute('href')
|
||||||
|
) {
|
||||||
// Link opener for files on the local system pointed to by href
|
// Link opener for files on the local system pointed to by href
|
||||||
const href = event.target.href
|
const href = event.target.href
|
||||||
const isLocalFile = href.startsWith('file:')
|
const isLocalFile = href.startsWith('file:')
|
||||||
@@ -94,31 +124,29 @@ const buildMarkdownPreviewContextMenu = function (markdownPreview, event) {
|
|||||||
const absPath = uri2path(href)
|
const absPath = uri2path(href)
|
||||||
try {
|
try {
|
||||||
if (fs.lstatSync(absPath).isFile()) {
|
if (fs.lstatSync(absPath).isFile()) {
|
||||||
template.push(
|
template.push({
|
||||||
{
|
|
||||||
label: i18n.__('Show in explorer'),
|
label: i18n.__('Show in explorer'),
|
||||||
click: (e) => shell.showItemInFolder(absPath)
|
click: e => shell.showItemInFolder(absPath)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Error while evaluating if the file is locally available', e)
|
console.log(
|
||||||
|
'Error while evaluating if the file is locally available',
|
||||||
|
e
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add option to context menu to copy url
|
// Add option to context menu to copy url
|
||||||
template.push(
|
template.push({
|
||||||
{
|
|
||||||
label: i18n.__('Copy Url'),
|
label: i18n.__('Copy Url'),
|
||||||
click: (e) => clipboard.writeText(href)
|
click: e => clipboard.writeText(href)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return Menu.buildFromTemplate(template)
|
return Menu.buildFromTemplate(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports =
|
module.exports = {
|
||||||
{
|
|
||||||
buildEditorContextMenu: buildEditorContextMenu,
|
buildEditorContextMenu: buildEditorContextMenu,
|
||||||
buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu
|
buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,19 @@ import 'codemirror-mode-elixir'
|
|||||||
|
|
||||||
const stylusCodeInfo = CodeMirror.modeInfo.find(info => info.name === 'Stylus')
|
const stylusCodeInfo = CodeMirror.modeInfo.find(info => info.name === 'Stylus')
|
||||||
if (stylusCodeInfo == null) {
|
if (stylusCodeInfo == null) {
|
||||||
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
CodeMirror.modeInfo.push({
|
||||||
|
name: 'Stylus',
|
||||||
|
mime: 'text/x-styl',
|
||||||
|
mode: 'stylus',
|
||||||
|
ext: ['styl'],
|
||||||
|
alias: ['styl']
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
stylusCodeInfo.alias = ['styl']
|
stylusCodeInfo.alias = ['styl']
|
||||||
}
|
}
|
||||||
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})
|
CodeMirror.modeInfo.push({
|
||||||
|
name: 'Elixir',
|
||||||
|
mime: 'text/x-elixir',
|
||||||
|
mode: 'elixir',
|
||||||
|
ext: ['ex']
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') {
|
export function findNoteTitle(
|
||||||
|
value,
|
||||||
|
enableFrontMatterTitle,
|
||||||
|
frontMatterTitleField = 'title'
|
||||||
|
) {
|
||||||
const splitted = value.split('\n')
|
const splitted = value.split('\n')
|
||||||
let title = null
|
let title = null
|
||||||
let isInsideCodeBlock = false
|
let isInsideCodeBlock = false
|
||||||
@@ -6,8 +10,13 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
|
|||||||
if (splitted[0] === '---') {
|
if (splitted[0] === '---') {
|
||||||
let line = 0
|
let line = 0
|
||||||
while (++line < splitted.length) {
|
while (++line < splitted.length) {
|
||||||
if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) {
|
if (
|
||||||
title = splitted[line].substring(frontMatterTitleField.length + 1).trim()
|
enableFrontMatterTitle &&
|
||||||
|
splitted[line].startsWith(frontMatterTitleField + ':')
|
||||||
|
) {
|
||||||
|
title = splitted[line]
|
||||||
|
.substring(frontMatterTitleField.length + 1)
|
||||||
|
.trim()
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -22,11 +31,15 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
|
|||||||
if (title === null) {
|
if (title === null) {
|
||||||
splitted.some((line, index) => {
|
splitted.some((line, index) => {
|
||||||
const trimmedLine = line.trim()
|
const trimmedLine = line.trim()
|
||||||
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
const trimmedNextLine =
|
||||||
|
splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
||||||
if (trimmedLine.match('```')) {
|
if (trimmedLine.match('```')) {
|
||||||
isInsideCodeBlock = !isInsideCodeBlock
|
isInsideCodeBlock = !isInsideCodeBlock
|
||||||
}
|
}
|
||||||
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
|
if (
|
||||||
|
isInsideCodeBlock === false &&
|
||||||
|
(trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))
|
||||||
|
) {
|
||||||
title = trimmedLine
|
title = trimmedLine
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -35,7 +48,7 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
|
|||||||
|
|
||||||
if (title === null) {
|
if (title === null) {
|
||||||
title = ''
|
title = ''
|
||||||
splitted.some((line) => {
|
splitted.some(line => {
|
||||||
if (line.trim().length > 0) {
|
if (line.trim().length > 0) {
|
||||||
title = line.trim()
|
title = line.trim()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ const _ = require('lodash')
|
|||||||
|
|
||||||
export function findStorage(storageKey) {
|
export function findStorage(storageKey) {
|
||||||
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
if (!_.isArray(cachedStorageList))
|
||||||
|
throw new Error("Target storage doesn't exist.")
|
||||||
const storage = _.find(cachedStorageList, { key: storageKey })
|
const storage = _.find(cachedStorageList, { key: storageKey })
|
||||||
if (storage === undefined) throw new Error('Target storage doesn\'t exist.')
|
if (storage === undefined) throw new Error("Target storage doesn't exist.")
|
||||||
|
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export function getTodoStatus (content) {
|
|||||||
let numberOfTodo = 0
|
let numberOfTodo = 0
|
||||||
let numberOfCompletedTodo = 0
|
let numberOfCompletedTodo = 0
|
||||||
|
|
||||||
splitted.forEach((line) => {
|
splitted.forEach(line => {
|
||||||
const trimmedLine = line.trim().replace(/^(>\s*)*/, '')
|
const trimmedLine = line.trim().replace(/^(>\s*)*/, '')
|
||||||
if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) {
|
if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) {
|
||||||
numberOfTodo++
|
numberOfTodo++
|
||||||
@@ -21,5 +21,5 @@ export function getTodoStatus (content) {
|
|||||||
|
|
||||||
export function getTodoPercentageOfCompleted(content) {
|
export function getTodoPercentageOfCompleted(content) {
|
||||||
const state = getTodoStatus(content)
|
const state = getTodoStatus(content)
|
||||||
return Math.floor(state.completed / state.total * 100)
|
return Math.floor((state.completed / state.total) * 100)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
export function decodeEntities(text) {
|
export function decodeEntities(text) {
|
||||||
var entities = [
|
var entities = [
|
||||||
['apos', '\''],
|
['apos', "'"],
|
||||||
['amp', '&'],
|
['amp', '&'],
|
||||||
['lt', '<'],
|
['lt', '<'],
|
||||||
['gt', '>'],
|
['gt', '>'],
|
||||||
@@ -26,14 +26,14 @@ export function decodeEntities (text) {
|
|||||||
|
|
||||||
export function encodeEntities(text) {
|
export function encodeEntities(text) {
|
||||||
const entities = [
|
const entities = [
|
||||||
['\'', 'apos'],
|
["'", 'apos'],
|
||||||
['<', 'lt'],
|
['<', 'lt'],
|
||||||
['>', 'gt'],
|
['>', 'gt'],
|
||||||
['\\?', '#63'],
|
['\\?', '#63'],
|
||||||
['\\$', '#36']
|
['\\$', '#36']
|
||||||
]
|
]
|
||||||
|
|
||||||
entities.forEach((entity) => {
|
entities.forEach(entity => {
|
||||||
text = text.replace(new RegExp(entity[0], 'g'), `&${entity[1]};`)
|
text = text.replace(new RegExp(entity[0], 'g'), `&${entity[1]};`)
|
||||||
})
|
})
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const i18n = new (require('i18n-2'))({
|
|||||||
// setup some locales - other locales default to the first locale
|
// setup some locales - other locales default to the first locale
|
||||||
locales: getLocales(),
|
locales: getLocales(),
|
||||||
extension: '.json',
|
extension: '.json',
|
||||||
directory: process.env.NODE_ENV === 'production'
|
directory:
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
? path.join(app.getAppPath(), './locales')
|
? path.join(app.getAppPath(), './locales')
|
||||||
: path.resolve('./locales'),
|
: path.resolve('./locales'),
|
||||||
devMode: false
|
devMode: false
|
||||||
|
|||||||
@@ -9,16 +9,22 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
let start = state.bMarks[line] + state.tShift[line]
|
let start = state.bMarks[line] + state.tShift[line]
|
||||||
const max = state.eMarks[line]
|
const max = state.eMarks[line]
|
||||||
|
|
||||||
if (start >= max) { return -1 }
|
if (start >= max) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
// Check bullet
|
// Check bullet
|
||||||
const marker = state.src.charCodeAt(start++)
|
const marker = state.src.charCodeAt(start++)
|
||||||
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 }
|
if (marker !== 0x7e /* ~ */ && marker !== 0x3a /* : */) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
const pos = state.skipSpaces(start)
|
const pos = state.skipSpaces(start)
|
||||||
|
|
||||||
// require space after ":"
|
// require space after ":"
|
||||||
if (start === pos) { return -1 }
|
if (start === pos) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
return start
|
return start
|
||||||
}
|
}
|
||||||
@@ -29,7 +35,10 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
let i
|
let i
|
||||||
let l
|
let l
|
||||||
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
||||||
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
|
if (
|
||||||
|
state.tokens[i].level === level &&
|
||||||
|
state.tokens[i].type === 'paragraph_open'
|
||||||
|
) {
|
||||||
state.tokens[i + 2].hidden = true
|
state.tokens[i + 2].hidden = true
|
||||||
state.tokens[i].hidden = true
|
state.tokens[i].hidden = true
|
||||||
i += 2
|
i += 2
|
||||||
@@ -63,21 +72,31 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
|
|
||||||
if (silent) {
|
if (silent) {
|
||||||
// quirk: validation mode validates a dd block only, not a whole deflist
|
// quirk: validation mode validates a dd block only, not a whole deflist
|
||||||
if (state.ddIndent < 0) { return false }
|
if (state.ddIndent < 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return skipMarker(state, startLine) >= 0
|
return skipMarker(state, startLine) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
nextLine = startLine + 1
|
nextLine = startLine + 1
|
||||||
if (nextLine >= endLine) { return false }
|
if (nextLine >= endLine) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (state.isEmpty(nextLine)) {
|
if (state.isEmpty(nextLine)) {
|
||||||
nextLine++
|
nextLine++
|
||||||
if (nextLine >= endLine) { return false }
|
if (nextLine >= endLine) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.sCount[nextLine] < state.blkIndent) { return false }
|
if (state.sCount[nextLine] < state.blkIndent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
contentStart = skipMarker(state, nextLine)
|
contentStart = skipMarker(state, nextLine)
|
||||||
if (contentStart < 0) { return false }
|
if (contentStart < 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Start list
|
// Start list
|
||||||
listTokIdx = state.tokens.length
|
listTokIdx = state.tokens.length
|
||||||
@@ -100,8 +119,7 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
// needed to break out of the second one
|
// needed to break out of the second one
|
||||||
//
|
//
|
||||||
/* eslint no-labels:0,block-scoped-var:0 */
|
/* eslint no-labels:0,block-scoped-var:0 */
|
||||||
OUTER:
|
OUTER: for (;;) {
|
||||||
for (;;) {
|
|
||||||
prevEmptyEnd = false
|
prevEmptyEnd = false
|
||||||
|
|
||||||
token = state.push('dt_open', 'dt', 1)
|
token = state.push('dt_open', 'dt', 1)
|
||||||
@@ -109,7 +127,9 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
|
|
||||||
token = state.push('inline', '', 0)
|
token = state.push('inline', '', 0)
|
||||||
token.map = [dtLine, dtLine]
|
token.map = [dtLine, dtLine]
|
||||||
token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim()
|
token.content = state
|
||||||
|
.getLines(dtLine, dtLine + 1, state.blkIndent, false)
|
||||||
|
.trim()
|
||||||
token.children = []
|
token.children = []
|
||||||
|
|
||||||
token = state.push('dt_close', 'dt', -1)
|
token = state.push('dt_close', 'dt', -1)
|
||||||
@@ -120,14 +140,17 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
|
|
||||||
pos = contentStart
|
pos = contentStart
|
||||||
max = state.eMarks[ddLine]
|
max = state.eMarks[ddLine]
|
||||||
offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine])
|
offset =
|
||||||
|
state.sCount[ddLine] +
|
||||||
|
contentStart -
|
||||||
|
(state.bMarks[ddLine] + state.tShift[ddLine])
|
||||||
|
|
||||||
while (pos < max) {
|
while (pos < max) {
|
||||||
ch = state.src.charCodeAt(pos)
|
ch = state.src.charCodeAt(pos)
|
||||||
|
|
||||||
if (isSpace(ch)) {
|
if (isSpace(ch)) {
|
||||||
if (ch === 0x09) {
|
if (ch === 0x09) {
|
||||||
offset += 4 - offset % 4
|
offset += 4 - (offset % 4)
|
||||||
} else {
|
} else {
|
||||||
offset++
|
offset++
|
||||||
}
|
}
|
||||||
@@ -153,8 +176,11 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
state.parentType = 'deflist'
|
state.parentType = 'deflist'
|
||||||
|
|
||||||
newEndLine = ddLine
|
newEndLine = ddLine
|
||||||
while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) {
|
while (
|
||||||
}
|
++newEndLine < endLine &&
|
||||||
|
(state.sCount[newEndLine] >= state.sCount[ddLine] ||
|
||||||
|
state.isEmpty(newEndLine))
|
||||||
|
) {}
|
||||||
|
|
||||||
oldLineMax = state.lineMax
|
oldLineMax = state.lineMax
|
||||||
state.lineMax = newEndLine
|
state.lineMax = newEndLine
|
||||||
@@ -169,7 +195,7 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
}
|
}
|
||||||
// Item become loose if finish with empty line,
|
// Item become loose if finish with empty line,
|
||||||
// but we should filter last element, because it means list finish
|
// but we should filter last element, because it means list finish
|
||||||
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1)
|
prevEmptyEnd = state.line - ddLine > 1 && state.isEmpty(state.line - 1)
|
||||||
|
|
||||||
state.tShift[ddLine] = oldTShift
|
state.tShift[ddLine] = oldTShift
|
||||||
state.sCount[ddLine] = oldSCount
|
state.sCount[ddLine] = oldSCount
|
||||||
@@ -182,11 +208,17 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
|
|
||||||
itemLines[1] = nextLine = state.line
|
itemLines[1] = nextLine = state.line
|
||||||
|
|
||||||
if (nextLine >= endLine) { break OUTER }
|
if (nextLine >= endLine) {
|
||||||
|
break OUTER
|
||||||
|
}
|
||||||
|
|
||||||
if (state.sCount[nextLine] < state.blkIndent) { break OUTER }
|
if (state.sCount[nextLine] < state.blkIndent) {
|
||||||
|
break OUTER
|
||||||
|
}
|
||||||
contentStart = skipMarker(state, nextLine)
|
contentStart = skipMarker(state, nextLine)
|
||||||
if (contentStart < 0) { break }
|
if (contentStart < 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
ddLine = nextLine
|
ddLine = nextLine
|
||||||
|
|
||||||
@@ -194,20 +226,36 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
// insert DD tag and repeat checking
|
// insert DD tag and repeat checking
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextLine >= endLine) { break }
|
if (nextLine >= endLine) {
|
||||||
|
break
|
||||||
|
}
|
||||||
dtLine = nextLine
|
dtLine = nextLine
|
||||||
|
|
||||||
if (state.isEmpty(dtLine)) { break }
|
if (state.isEmpty(dtLine)) {
|
||||||
if (state.sCount[dtLine] < state.blkIndent) { break }
|
break
|
||||||
|
}
|
||||||
|
if (state.sCount[dtLine] < state.blkIndent) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
ddLine = dtLine + 1
|
ddLine = dtLine + 1
|
||||||
if (ddLine >= endLine) { break }
|
if (ddLine >= endLine) {
|
||||||
if (state.isEmpty(ddLine)) { ddLine++ }
|
break
|
||||||
if (ddLine >= endLine) { break }
|
}
|
||||||
|
if (state.isEmpty(ddLine)) {
|
||||||
|
ddLine++
|
||||||
|
}
|
||||||
|
if (ddLine >= endLine) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
if (state.sCount[ddLine] < state.blkIndent) { break }
|
if (state.sCount[ddLine] < state.blkIndent) {
|
||||||
|
break
|
||||||
|
}
|
||||||
contentStart = skipMarker(state, ddLine)
|
contentStart = skipMarker(state, ddLine)
|
||||||
if (contentStart < 0) { break }
|
if (contentStart < 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// go to the next loop iteration:
|
// go to the next loop iteration:
|
||||||
// insert DT and DD tags and repeat checking
|
// insert DT and DD tags and repeat checking
|
||||||
@@ -228,5 +276,7 @@ module.exports = function definitionListPlugin (md) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] })
|
md.block.ruler.before('paragraph', 'deflist', deflist, {
|
||||||
|
alt: ['paragraph', 'reference']
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ module.exports = function (md, renderers, defaultRenderer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const marker = state.src.charCodeAt(pos)
|
const marker = state.src.charCodeAt(pos)
|
||||||
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) {
|
if (marker !== 0x7e /* ~ */ && marker !== 0x60 /* ` */) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,10 @@ module.exports = function (md, renderers, defaultRenderer) {
|
|||||||
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
|
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) {
|
if (
|
||||||
|
state.src.charCodeAt(pos) !== marker ||
|
||||||
|
state.sCount[nextLine] - state.blkIndent >= 4
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,10 +130,12 @@ module.exports = function (md, renderers, defaultRenderer) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for (const name in renderers) {
|
for (const name in renderers) {
|
||||||
md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index])
|
md.renderer.rules[`${name}_fence`] = (tokens, index) =>
|
||||||
|
renderers[name](tokens[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defaultRenderer) {
|
if (defaultRenderer) {
|
||||||
md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index])
|
md.renderer.rules['_fence'] = (tokens, index) =>
|
||||||
|
defaultRenderer(tokens[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,18 @@
|
|||||||
|
|
||||||
module.exports = function frontMatterPlugin(md) {
|
module.exports = function frontMatterPlugin(md) {
|
||||||
function frontmatter(state, startLine, endLine, silent) {
|
function frontmatter(state, startLine, endLine, silent) {
|
||||||
if (startLine !== 0 || state.src.substr(startLine, state.eMarks[0]) !== '---') {
|
if (
|
||||||
|
startLine !== 0 ||
|
||||||
|
state.src.substr(startLine, state.eMarks[0]) !== '---'
|
||||||
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let line = 0
|
let line = 0
|
||||||
while (++line < state.lineMax) {
|
while (++line < state.lineMax) {
|
||||||
if (state.src.substring(state.bMarks[line], state.eMarks[line]) === '---') {
|
if (
|
||||||
|
state.src.substring(state.bMarks[line], state.eMarks[line]) === '---'
|
||||||
|
) {
|
||||||
state.line = line + 1
|
state.line = line + 1
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ module.exports = function sanitizePlugin (md, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
|
const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
|
||||||
const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig
|
const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/gi
|
||||||
|
|
||||||
function sanitizeInline(html, options) {
|
function sanitizeInline(html, options) {
|
||||||
let match = tagRegex.exec(html)
|
let match = tagRegex.exec(html)
|
||||||
@@ -46,7 +46,12 @@ function sanitizeInline (html, options) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options
|
const {
|
||||||
|
allowedTags,
|
||||||
|
allowedAttributes,
|
||||||
|
selfClosing,
|
||||||
|
allowedSchemesAppliedToAttributes
|
||||||
|
} = options
|
||||||
|
|
||||||
if (match[1] !== undefined) {
|
if (match[1] !== undefined) {
|
||||||
// opening tag
|
// opening tag
|
||||||
@@ -65,9 +70,17 @@ function sanitizeInline (html, options) {
|
|||||||
name = match[1].toLowerCase()
|
name = match[1].toLowerCase()
|
||||||
value = match[3]
|
value = match[3]
|
||||||
|
|
||||||
if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
|
if (
|
||||||
|
allowedAttributes['*'].indexOf(name) !== -1 ||
|
||||||
|
(allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)
|
||||||
|
) {
|
||||||
if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
|
if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
|
||||||
if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) {
|
if (
|
||||||
|
naughtyHRef(value, options) ||
|
||||||
|
(tag === 'iframe' &&
|
||||||
|
name === 'src' &&
|
||||||
|
naughtyIFrame(value, options))
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ export function generateInEditor (editor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addTocAtCursorPosition() {
|
function addTocAtCursorPosition() {
|
||||||
const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity}))
|
const toc = generate(
|
||||||
|
editor.getRange(editor.getCursor(), { line: Infinity })
|
||||||
|
)
|
||||||
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +90,10 @@ export function generate (markdownText) {
|
|||||||
|
|
||||||
function wrapTocWithEol(toc, editor) {
|
function wrapTocWithEol(toc, editor) {
|
||||||
const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
|
const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
|
||||||
const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL
|
const rightWrap =
|
||||||
|
editor.getLine(editor.getCursor().line).length === editor.getCursor().ch
|
||||||
|
? ''
|
||||||
|
: EOL
|
||||||
return leftWrap + toc + rightWrap
|
return leftWrap + toc + rightWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ function createGutter (str, firstLineNumber) {
|
|||||||
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
|
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
|
||||||
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
||||||
}
|
}
|
||||||
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
return (
|
||||||
|
'<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Markdown {
|
class Markdown {
|
||||||
@@ -37,29 +39,129 @@ class Markdown {
|
|||||||
this.md.linkify.set({ fuzzyLink: false })
|
this.md.linkify.set({ fuzzyLink: false })
|
||||||
|
|
||||||
if (updatedOptions.sanitize !== 'NONE') {
|
if (updatedOptions.sanitize !== 'NONE') {
|
||||||
const allowedTags = ['iframe', 'input', 'b',
|
const allowedTags = [
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
|
'iframe',
|
||||||
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote',
|
'input',
|
||||||
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
|
'b',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'h7',
|
||||||
|
'h8',
|
||||||
|
'br',
|
||||||
|
'b',
|
||||||
|
'i',
|
||||||
|
'strong',
|
||||||
|
'em',
|
||||||
|
'a',
|
||||||
|
'pre',
|
||||||
|
'code',
|
||||||
|
'img',
|
||||||
|
'tt',
|
||||||
|
'div',
|
||||||
|
'ins',
|
||||||
|
'del',
|
||||||
|
'sup',
|
||||||
|
'sub',
|
||||||
|
'p',
|
||||||
|
'ol',
|
||||||
|
'ul',
|
||||||
|
'table',
|
||||||
|
'thead',
|
||||||
|
'tbody',
|
||||||
|
'tfoot',
|
||||||
|
'blockquote',
|
||||||
|
'dl',
|
||||||
|
'dt',
|
||||||
|
'dd',
|
||||||
|
'kbd',
|
||||||
|
'q',
|
||||||
|
'samp',
|
||||||
|
'var',
|
||||||
|
'hr',
|
||||||
|
'ruby',
|
||||||
|
'rt',
|
||||||
|
'rp',
|
||||||
|
'li',
|
||||||
|
'tr',
|
||||||
|
'td',
|
||||||
|
'th',
|
||||||
|
's',
|
||||||
|
'strike',
|
||||||
|
'summary',
|
||||||
|
'details'
|
||||||
]
|
]
|
||||||
const allowedAttributes = [
|
const allowedAttributes = [
|
||||||
'abbr', 'accept', 'accept-charset',
|
'abbr',
|
||||||
'accesskey', 'action', 'align', 'alt', 'axis',
|
'accept',
|
||||||
'border', 'cellpadding', 'cellspacing', 'char',
|
'accept-charset',
|
||||||
'charoff', 'charset', 'checked',
|
'accesskey',
|
||||||
'clear', 'cols', 'colspan', 'color',
|
'action',
|
||||||
'compact', 'coords', 'datetime', 'dir',
|
'align',
|
||||||
'disabled', 'enctype', 'for', 'frame',
|
'alt',
|
||||||
'headers', 'height', 'hreflang',
|
'axis',
|
||||||
'hspace', 'ismap', 'label', 'lang',
|
'border',
|
||||||
'maxlength', 'media', 'method',
|
'cellpadding',
|
||||||
'multiple', 'name', 'nohref', 'noshade',
|
'cellspacing',
|
||||||
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
|
'char',
|
||||||
'rows', 'rowspan', 'rules', 'scope',
|
'charoff',
|
||||||
'selected', 'shape', 'size', 'span',
|
'charset',
|
||||||
'start', 'summary', 'tabindex', 'target',
|
'checked',
|
||||||
'title', 'type', 'usemap', 'valign', 'value',
|
'clear',
|
||||||
'vspace', 'width', 'itemprop'
|
'cols',
|
||||||
|
'colspan',
|
||||||
|
'color',
|
||||||
|
'compact',
|
||||||
|
'coords',
|
||||||
|
'datetime',
|
||||||
|
'dir',
|
||||||
|
'disabled',
|
||||||
|
'enctype',
|
||||||
|
'for',
|
||||||
|
'frame',
|
||||||
|
'headers',
|
||||||
|
'height',
|
||||||
|
'hreflang',
|
||||||
|
'hspace',
|
||||||
|
'ismap',
|
||||||
|
'label',
|
||||||
|
'lang',
|
||||||
|
'maxlength',
|
||||||
|
'media',
|
||||||
|
'method',
|
||||||
|
'multiple',
|
||||||
|
'name',
|
||||||
|
'nohref',
|
||||||
|
'noshade',
|
||||||
|
'nowrap',
|
||||||
|
'open',
|
||||||
|
'prompt',
|
||||||
|
'readonly',
|
||||||
|
'rel',
|
||||||
|
'rev',
|
||||||
|
'rows',
|
||||||
|
'rowspan',
|
||||||
|
'rules',
|
||||||
|
'scope',
|
||||||
|
'selected',
|
||||||
|
'shape',
|
||||||
|
'size',
|
||||||
|
'span',
|
||||||
|
'start',
|
||||||
|
'summary',
|
||||||
|
'tabindex',
|
||||||
|
'target',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
'usemap',
|
||||||
|
'valign',
|
||||||
|
'value',
|
||||||
|
'vspace',
|
||||||
|
'width',
|
||||||
|
'itemprop'
|
||||||
]
|
]
|
||||||
|
|
||||||
if (updatedOptions.sanitize === 'ALLOW_STYLES') {
|
if (updatedOptions.sanitize === 'ALLOW_STYLES') {
|
||||||
@@ -72,15 +174,15 @@ class Markdown {
|
|||||||
allowedTags,
|
allowedTags,
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
'*': allowedAttributes,
|
'*': allowedAttributes,
|
||||||
'a': ['href'],
|
a: ['href'],
|
||||||
'div': ['itemscope', 'itemtype'],
|
div: ['itemscope', 'itemtype'],
|
||||||
'blockquote': ['cite'],
|
blockquote: ['cite'],
|
||||||
'del': ['cite'],
|
del: ['cite'],
|
||||||
'ins': ['cite'],
|
ins: ['cite'],
|
||||||
'q': ['cite'],
|
q: ['cite'],
|
||||||
'img': ['src', 'width', 'height'],
|
img: ['src', 'width', 'height'],
|
||||||
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
||||||
'input': ['type', 'id', 'checked']
|
input: ['type', 'id', 'checked']
|
||||||
},
|
},
|
||||||
allowedIframeHostnames: ['www.youtube.com'],
|
allowedIframeHostnames: ['www.youtube.com'],
|
||||||
selfClosing: ['img', 'br', 'hr', 'input'],
|
selfClosing: ['img', 'br', 'hr', 'input'],
|
||||||
@@ -124,7 +226,19 @@ class Markdown {
|
|||||||
slugify: require('./slugify')
|
slugify: require('./slugify')
|
||||||
})
|
})
|
||||||
this.md.use(require('markdown-it-kbd'))
|
this.md.use(require('markdown-it-kbd'))
|
||||||
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error', 'quote', 'abstract', 'question']})
|
this.md.use(require('markdown-it-admonition'), {
|
||||||
|
types: [
|
||||||
|
'note',
|
||||||
|
'hint',
|
||||||
|
'attention',
|
||||||
|
'caution',
|
||||||
|
'danger',
|
||||||
|
'error',
|
||||||
|
'quote',
|
||||||
|
'abstract',
|
||||||
|
'question'
|
||||||
|
]
|
||||||
|
})
|
||||||
this.md.use(require('markdown-it-abbr'))
|
this.md.use(require('markdown-it-abbr'))
|
||||||
this.md.use(require('markdown-it-sub'))
|
this.md.use(require('markdown-it-sub'))
|
||||||
this.md.use(require('markdown-it-sup'))
|
this.md.use(require('markdown-it-sup'))
|
||||||
@@ -144,7 +258,9 @@ class Markdown {
|
|||||||
this.md.use(require('./markdown-it-deflist'))
|
this.md.use(require('./markdown-it-deflist'))
|
||||||
this.md.use(require('./markdown-it-frontmatter'))
|
this.md.use(require('./markdown-it-frontmatter'))
|
||||||
|
|
||||||
this.md.use(require('./markdown-it-fence'), {
|
this.md.use(
|
||||||
|
require('./markdown-it-fence'),
|
||||||
|
{
|
||||||
chart: token => {
|
chart: token => {
|
||||||
if (token.parameters.hasOwnProperty('yaml')) {
|
if (token.parameters.hasOwnProperty('yaml')) {
|
||||||
token.parameters.format = 'yaml'
|
token.parameters.format = 'yaml'
|
||||||
@@ -152,54 +268,75 @@ class Markdown {
|
|||||||
|
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="chart" data-height="${token.parameters.height}" data-format="${token.parameters.format || 'json'}">${token.content}</div>
|
<div class="chart" data-height="${
|
||||||
|
token.parameters.height
|
||||||
|
}" data-format="${token.parameters.format || 'json'}">${
|
||||||
|
token.content
|
||||||
|
}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
},
|
},
|
||||||
flowchart: token => {
|
flowchart: token => {
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
|
<div class="flowchart" data-height="${token.parameters.height}">${
|
||||||
|
token.content
|
||||||
|
}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
},
|
},
|
||||||
gallery: token => {
|
gallery: token => {
|
||||||
const content = token.content.split('\n').slice(0, -1).map(line => {
|
const content = token.content
|
||||||
|
.split('\n')
|
||||||
|
.slice(0, -1)
|
||||||
|
.map(line => {
|
||||||
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
|
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
|
||||||
if (match) {
|
if (match) {
|
||||||
return mdurl.encode(match[1])
|
return mdurl.encode(match[1])
|
||||||
} else {
|
} else {
|
||||||
return mdurl.encode(line)
|
return mdurl.encode(line)
|
||||||
}
|
}
|
||||||
}).join('\n')
|
})
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="gallery" data-autoplay="${token.parameters.autoplay}" data-height="${token.parameters.height}">${content}</div>
|
<div class="gallery" data-autoplay="${
|
||||||
|
token.parameters.autoplay
|
||||||
|
}" data-height="${token.parameters.height}">${content}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
},
|
},
|
||||||
mermaid: token => {
|
mermaid: token => {
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="mermaid" data-height="${token.parameters.height}">${token.content}</div>
|
<div class="mermaid" data-height="${token.parameters.height}">${
|
||||||
|
token.content
|
||||||
|
}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
},
|
},
|
||||||
sequence: token => {
|
sequence: token => {
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="sequence" data-height="${token.parameters.height}">${token.content}</div>
|
<div class="sequence" data-height="${token.parameters.height}">${
|
||||||
|
token.content
|
||||||
|
}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
}
|
}
|
||||||
}, token => {
|
},
|
||||||
|
token => {
|
||||||
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
|
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
${createGutter(token.content, token.firstLineNumber)}
|
${createGutter(token.content, token.firstLineNumber)}
|
||||||
<code class="${token.langType}">${token.content}</code>
|
<code class="${token.langType}">${token.content}</code>
|
||||||
</pre>`
|
</pre>`
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||||
const plantuml = require('markdown-it-plantuml')
|
const plantuml = require('markdown-it-plantuml')
|
||||||
const plantUmlStripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
const plantUmlStripTrailingSlash = url =>
|
||||||
const plantUmlServerAddress = plantUmlStripTrailingSlash(config.preview.plantUMLServerAddress)
|
url.endsWith('/') ? url.slice(0, -1) : url
|
||||||
|
const plantUmlServerAddress = plantUmlStripTrailingSlash(
|
||||||
|
config.preview.plantUMLServerAddress
|
||||||
|
)
|
||||||
const parsePlantUml = function(umlCode, openMarker, closeMarker, type) {
|
const parsePlantUml = function(umlCode, openMarker, closeMarker, type) {
|
||||||
const s = unescape(encodeURIComponent(umlCode))
|
const s = unescape(encodeURIComponent(umlCode))
|
||||||
const zippedCode = deflate.encode64(
|
const zippedCode = deflate.encode64(
|
||||||
@@ -209,39 +346,47 @@ class Markdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.md.use(plantuml, {
|
this.md.use(plantuml, {
|
||||||
generateSource: (umlCode) => parsePlantUml(umlCode, '@startuml', '@enduml', 'svg')
|
generateSource: umlCode =>
|
||||||
|
parsePlantUml(umlCode, '@startuml', '@enduml', 'svg')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
// Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
||||||
this.md.use(plantuml, {
|
this.md.use(plantuml, {
|
||||||
openMarker: '@startditaa',
|
openMarker: '@startditaa',
|
||||||
closeMarker: '@endditaa',
|
closeMarker: '@endditaa',
|
||||||
generateSource: (umlCode) => parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
|
generateSource: umlCode =>
|
||||||
|
parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mindmap support
|
// Mindmap support
|
||||||
this.md.use(plantuml, {
|
this.md.use(plantuml, {
|
||||||
openMarker: '@startmindmap',
|
openMarker: '@startmindmap',
|
||||||
closeMarker: '@endmindmap',
|
closeMarker: '@endmindmap',
|
||||||
generateSource: (umlCode) => parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg')
|
generateSource: umlCode =>
|
||||||
|
parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg')
|
||||||
})
|
})
|
||||||
|
|
||||||
// WBS support
|
// WBS support
|
||||||
this.md.use(plantuml, {
|
this.md.use(plantuml, {
|
||||||
openMarker: '@startwbs',
|
openMarker: '@startwbs',
|
||||||
closeMarker: '@endwbs',
|
closeMarker: '@endwbs',
|
||||||
generateSource: (umlCode) => parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg')
|
generateSource: umlCode =>
|
||||||
|
parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Gantt support
|
// Gantt support
|
||||||
this.md.use(plantuml, {
|
this.md.use(plantuml, {
|
||||||
openMarker: '@startgantt',
|
openMarker: '@startgantt',
|
||||||
closeMarker: '@endgantt',
|
closeMarker: '@endgantt',
|
||||||
generateSource: (umlCode) => parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg')
|
generateSource: umlCode =>
|
||||||
|
parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Override task item
|
// Override task item
|
||||||
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
this.md.block.ruler.at('paragraph', function(
|
||||||
|
state,
|
||||||
|
startLine /*, endLine */
|
||||||
|
) {
|
||||||
let content, terminate, i, l, token
|
let content, terminate, i, l, token
|
||||||
let nextLine = startLine + 1
|
let nextLine = startLine + 1
|
||||||
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||||
@@ -251,10 +396,14 @@ class Markdown {
|
|||||||
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
||||||
// this would be a code block normally, but after paragraph
|
// this would be a code block normally, but after paragraph
|
||||||
// it's considered a lazy continuation regardless of what's there
|
// it's considered a lazy continuation regardless of what's there
|
||||||
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
|
if (state.sCount[nextLine] - state.blkIndent > 3) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// quirk for blockquotes, this line should already be checked by that rule
|
// quirk for blockquotes, this line should already be checked by that rule
|
||||||
if (state.sCount[nextLine] < 0) { continue }
|
if (state.sCount[nextLine] < 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Some tags can terminate paragraph without empty line.
|
// Some tags can terminate paragraph without empty line.
|
||||||
terminate = false
|
terminate = false
|
||||||
@@ -264,10 +413,14 @@ class Markdown {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (terminate) { break }
|
if (terminate) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
content = state
|
||||||
|
.getLines(startLine, nextLine, state.blkIndent, false)
|
||||||
|
.trim()
|
||||||
|
|
||||||
state.line = nextLine
|
state.line = nextLine
|
||||||
|
|
||||||
@@ -277,18 +430,31 @@ class Markdown {
|
|||||||
if (state.parentType === 'list') {
|
if (state.parentType === 'list') {
|
||||||
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||||
if (match) {
|
if (match) {
|
||||||
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
|
const liToken = lastFindInArray(
|
||||||
|
state.tokens,
|
||||||
|
token => token.type === 'list_item_open'
|
||||||
|
)
|
||||||
if (liToken) {
|
if (liToken) {
|
||||||
if (!liToken.attrs) {
|
if (!liToken.attrs) {
|
||||||
liToken.attrs = []
|
liToken.attrs = []
|
||||||
}
|
}
|
||||||
if (config.preview.lineThroughCheckbox) {
|
if (config.preview.lineThroughCheckbox) {
|
||||||
liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`])
|
liToken.attrs.push([
|
||||||
|
'class',
|
||||||
|
`taskListItem${match[1] !== ' ' ? ' checked' : ''}`
|
||||||
|
])
|
||||||
} else {
|
} else {
|
||||||
liToken.attrs.push(['class', 'taskListItem'])
|
liToken.attrs.push(['class', 'taskListItem'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
content = `<label class='taskListItem${
|
||||||
|
match[1] !== ' ' ? ' checked' : ''
|
||||||
|
}' for='checkbox-${startLine + 1}'><input type='checkbox'${
|
||||||
|
match[1] !== ' ' ? ' checked' : ''
|
||||||
|
} id='checkbox-${startLine + 1}'/> ${content.substring(
|
||||||
|
4,
|
||||||
|
content.length
|
||||||
|
)}</label>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +475,7 @@ class Markdown {
|
|||||||
// Add line number attribute for scrolling
|
// Add line number attribute for scrolling
|
||||||
const originalRender = this.md.renderer.render
|
const originalRender = this.md.renderer.render
|
||||||
this.md.renderer.render = (tokens, options, env) => {
|
this.md.renderer.render = (tokens, options, env) => {
|
||||||
tokens.forEach((token) => {
|
tokens.forEach(token => {
|
||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
case 'blockquote_open':
|
case 'blockquote_open':
|
||||||
case 'dd_open':
|
case 'dd_open':
|
||||||
|
|||||||
@@ -4,12 +4,22 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
|||||||
import queryString from 'query-string'
|
import queryString from 'query-string'
|
||||||
import { push } from 'connected-react-router'
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
|
export function createMarkdownNote(
|
||||||
|
storage,
|
||||||
|
folder,
|
||||||
|
dispatch,
|
||||||
|
location,
|
||||||
|
params,
|
||||||
|
config
|
||||||
|
) {
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||||
|
|
||||||
let tags = []
|
let tags = []
|
||||||
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
|
if (
|
||||||
|
config.ui.tagNewNoteWithFilteringTags &&
|
||||||
|
location.pathname.match(/\/tags/)
|
||||||
|
) {
|
||||||
tags = params.tagname.split(' ')
|
tags = params.tagname.split(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,25 +39,40 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
|
|||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
|
|
||||||
dispatch(push({
|
dispatch(
|
||||||
|
push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: queryString.stringify({ key: noteHash })
|
search: queryString.stringify({ key: noteHash })
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
ee.emit('list:jump', noteHash)
|
ee.emit('list:jump', noteHash)
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSnippetNote (storage, folder, dispatch, location, params, config) {
|
export function createSnippetNote(
|
||||||
|
storage,
|
||||||
|
folder,
|
||||||
|
dispatch,
|
||||||
|
location,
|
||||||
|
params,
|
||||||
|
config
|
||||||
|
) {
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||||
|
|
||||||
let tags = []
|
let tags = []
|
||||||
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
|
if (
|
||||||
|
config.ui.tagNewNoteWithFilteringTags &&
|
||||||
|
location.pathname.match(/\/tags/)
|
||||||
|
) {
|
||||||
tags = params.tagname.split(' ')
|
tags = params.tagname.split(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLanguage = config.editor.snippetDefaultLanguage === 'Auto Detect' ? null : config.editor.snippetDefaultLanguage
|
const defaultLanguage =
|
||||||
|
config.editor.snippetDefaultLanguage === 'Auto Detect'
|
||||||
|
? null
|
||||||
|
: config.editor.snippetDefaultLanguage
|
||||||
|
|
||||||
return dataApi
|
return dataApi
|
||||||
.createNote(storage, {
|
.createNote(storage, {
|
||||||
@@ -71,10 +96,12 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
|
|||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
dispatch(push({
|
dispatch(
|
||||||
|
push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: queryString.stringify({ key: noteHash })
|
search: queryString.stringify({ key: noteHash })
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
ee.emit('list:jump', noteHash)
|
ee.emit('list:jump', noteHash)
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import _ from 'lodash'
|
|||||||
|
|
||||||
export default function searchFromNotes(notes, search) {
|
export default function searchFromNotes(notes, search) {
|
||||||
if (search.trim().length === 0) return []
|
if (search.trim().length === 0) return []
|
||||||
const searchBlocks = search.split(' ').filter(block => { return block !== '' })
|
const searchBlocks = search.split(' ').filter(block => {
|
||||||
|
return block !== ''
|
||||||
|
})
|
||||||
|
|
||||||
let foundNotes = notes
|
let foundNotes = notes
|
||||||
searchBlocks.forEach((block) => {
|
searchBlocks.forEach(block => {
|
||||||
foundNotes = findByWordOrTag(foundNotes, block)
|
foundNotes = findByWordOrTag(foundNotes, block)
|
||||||
})
|
})
|
||||||
return foundNotes
|
return foundNotes
|
||||||
@@ -18,14 +20,19 @@ function findByWordOrTag (notes, block) {
|
|||||||
}
|
}
|
||||||
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
|
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||||
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
|
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||||
return notes.filter((note) => {
|
return notes.filter(note => {
|
||||||
if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) {
|
if (_.isArray(note.tags) && note.tags.some(_tag => _tag.match(tagRegExp))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (note.type === 'SNIPPET_NOTE') {
|
if (note.type === 'SNIPPET_NOTE') {
|
||||||
return note.description.match(wordRegExp) || note.snippets.some((snippet) => {
|
return (
|
||||||
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
|
note.description.match(wordRegExp) ||
|
||||||
|
note.snippets.some(snippet => {
|
||||||
|
return (
|
||||||
|
snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
)
|
||||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||||
return note.content.match(wordRegExp)
|
return note.content.match(wordRegExp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
module.exports = function slugify(title) {
|
module.exports = function slugify(title) {
|
||||||
const slug = encodeURI(
|
const slug = encodeURI(
|
||||||
title.trim()
|
title
|
||||||
|
.trim()
|
||||||
.replace(/^\s+/, '')
|
.replace(/^\s+/, '')
|
||||||
.replace(/\s+$/, '')
|
.replace(/\s+$/, '')
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g, '')
|
.replace(
|
||||||
|
/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g,
|
||||||
|
''
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return slug
|
return slug
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ function setLanguage (editor, lang) {
|
|||||||
dictionary = new Typo(lang, false, false, {
|
dictionary = new Typo(lang, false, false, {
|
||||||
dictionaryPath: DICTIONARY_PATH,
|
dictionaryPath: DICTIONARY_PATH,
|
||||||
asyncLoad: true,
|
asyncLoad: true,
|
||||||
loadedCallback: () =>
|
loadedCallback: () => checkWholeDocument(editor)
|
||||||
checkWholeDocument(editor)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +76,10 @@ function checkWholeDocument (editor) {
|
|||||||
*/
|
*/
|
||||||
function checkMultiLineRange(editor, from, to) {
|
function checkMultiLineRange(editor, from, to) {
|
||||||
function sortRange(pos1, pos2) {
|
function sortRange(pos1, pos2) {
|
||||||
if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) {
|
if (
|
||||||
|
pos1.line > pos2.line ||
|
||||||
|
(pos1.line === pos2.line && pos1.ch > pos2.ch)
|
||||||
|
) {
|
||||||
return { from: pos2, to: pos1 }
|
return { from: pos2, to: pos1 }
|
||||||
}
|
}
|
||||||
return { from: pos1, to: pos2 }
|
return { from: pos1, to: pos2 }
|
||||||
@@ -97,7 +99,7 @@ function checkMultiLineRange (editor, from, to) {
|
|||||||
while (w <= wEnd) {
|
while (w <= wEnd) {
|
||||||
const wordRange = editor.findWordAt({ line: l, ch: w })
|
const wordRange = editor.findWordAt({ line: l, ch: w })
|
||||||
self.checkWord(editor, wordRange)
|
self.checkWord(editor, wordRange)
|
||||||
w += (wordRange.head.ch - wordRange.anchor.ch) + 1
|
w += wordRange.head.ch - wordRange.anchor.ch + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +118,9 @@ function checkWord (editor, wordRange) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!dictionary.check(word)) {
|
if (!dictionary.check(word)) {
|
||||||
editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]})
|
editor.markText(wordRange.anchor, wordRange.head, {
|
||||||
|
className: styles[CSS_ERROR_CLASS]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,17 +142,25 @@ function checkChangeRange (editor, fromChangeObject, toChangeObject) {
|
|||||||
let smallest = start.from
|
let smallest = start.from
|
||||||
let biggest = end.to
|
let biggest = end.to
|
||||||
for (const currentPos of possiblePositions) {
|
for (const currentPos of possiblePositions) {
|
||||||
if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) {
|
if (
|
||||||
|
currentPos.line < smallest.line ||
|
||||||
|
(currentPos.line === smallest.line && currentPos.ch < smallest.ch)
|
||||||
|
) {
|
||||||
smallest = currentPos
|
smallest = currentPos
|
||||||
}
|
}
|
||||||
if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) {
|
if (
|
||||||
|
currentPos.line > biggest.line ||
|
||||||
|
(currentPos.line === biggest.line && currentPos.ch > biggest.ch)
|
||||||
|
) {
|
||||||
biggest = currentPos
|
biggest = currentPos
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { start: smallest, end: biggest }
|
return { start: smallest, end: biggest }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dictionary === null || editor == null) { return }
|
if (dictionary === null || editor == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { start, end } = getStartAndEnd(fromChangeObject, toChangeObject)
|
const { start, end } = getStartAndEnd(fromChangeObject, toChangeObject)
|
||||||
@@ -165,7 +177,10 @@ function checkChangeRange (editor, fromChangeObject, toChangeObject) {
|
|||||||
|
|
||||||
self.checkMultiLineRange(editor, start, end)
|
self.checkMultiLineRange(editor, start, end)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', e)
|
console.info(
|
||||||
|
'Error during the spell check. It might be due to problems figuring out the range of the new text..',
|
||||||
|
e
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,14 +188,22 @@ function saveLiveSpellCheckFrom (changeObject) {
|
|||||||
liveSpellCheckFrom = changeObject
|
liveSpellCheckFrom = changeObject
|
||||||
}
|
}
|
||||||
let liveSpellCheckFrom
|
let liveSpellCheckFrom
|
||||||
const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, {
|
const debouncedSpellCheckLeading = _.debounce(
|
||||||
'leading': true,
|
saveLiveSpellCheckFrom,
|
||||||
'trailing': false
|
MILLISECONDS_TILL_LIVECHECK,
|
||||||
})
|
{
|
||||||
const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, {
|
leading: true,
|
||||||
'leading': false,
|
trailing: false
|
||||||
'trailing': true
|
}
|
||||||
})
|
)
|
||||||
|
const debouncedSpellCheck = _.debounce(
|
||||||
|
checkChangeRange,
|
||||||
|
MILLISECONDS_TILL_LIVECHECK,
|
||||||
|
{
|
||||||
|
leading: false,
|
||||||
|
trailing: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
|
* Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
|
||||||
|
|||||||
@@ -133,7 +133,9 @@ export function isObjectEqual (a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isMarkdownTitleURL(str) {
|
export function isMarkdownTitleURL(str) {
|
||||||
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
|
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(
|
||||||
|
str
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function humanFileSize(bytes) {
|
export function humanFileSize(bytes) {
|
||||||
|
|||||||
@@ -25,12 +25,15 @@ class FolderSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e) {
|
handleClick(e) {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'SEARCH',
|
status: 'SEARCH',
|
||||||
optionIndex: -1
|
optionIndex: -1
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.search.focus()
|
this.refs.search.focus()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFocus(e) {
|
handleFocus(e) {
|
||||||
@@ -53,30 +56,39 @@ class FolderSelect extends React.Component {
|
|||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
case 13:
|
case 13:
|
||||||
if (this.state.status === 'FOCUS') {
|
if (this.state.status === 'FOCUS') {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'SEARCH',
|
status: 'SEARCH',
|
||||||
optionIndex: -1
|
optionIndex: -1
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.search.focus()
|
this.refs.search.focus()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 40:
|
case 40:
|
||||||
case 38:
|
case 38:
|
||||||
if (this.state.status === 'FOCUS') {
|
if (this.state.status === 'FOCUS') {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'SEARCH',
|
status: 'SEARCH',
|
||||||
optionIndex: 0
|
optionIndex: 0
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.search.focus()
|
this.refs.search.focus()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 9:
|
case 9:
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])')
|
const tabbable = document.querySelectorAll(
|
||||||
const previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
|
'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])'
|
||||||
|
)
|
||||||
|
const previousEl =
|
||||||
|
tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
|
||||||
if (previousEl != null) previousEl.focus()
|
if (previousEl != null) previousEl.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +105,12 @@ class FolderSelect extends React.Component {
|
|||||||
handleSearchInputChange(e) {
|
handleSearchInputChange(e) {
|
||||||
const { folders } = this.props
|
const { folders } = this.props
|
||||||
const search = this.refs.search.value
|
const search = this.refs.search.value
|
||||||
const optionIndex = search.length > 0
|
const optionIndex =
|
||||||
? _.findIndex(folders, (folder) => {
|
search.length > 0
|
||||||
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
|
? _.findIndex(folders, folder => {
|
||||||
|
return folder.name.match(
|
||||||
|
new RegExp('^' + _.escapeRegExp(search), 'i')
|
||||||
|
)
|
||||||
})
|
})
|
||||||
: -1
|
: -1
|
||||||
|
|
||||||
@@ -121,11 +136,14 @@ class FolderSelect extends React.Component {
|
|||||||
break
|
break
|
||||||
case 27:
|
case 27:
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'FOCUS'
|
status: 'FOCUS'
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.root.focus()
|
this.refs.root.focus()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,24 +177,30 @@ class FolderSelect extends React.Component {
|
|||||||
|
|
||||||
const folder = folders[optionIndex]
|
const folder = folders[optionIndex]
|
||||||
if (folder != null) {
|
if (folder != null) {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'FOCUS'
|
status: 'FOCUS'
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.setValue(folder.key)
|
this.setValue(folder.key)
|
||||||
this.refs.root.focus()
|
this.refs.root.focus()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOptionClick(storageKey, folderKey) {
|
handleOptionClick(storageKey, folderKey) {
|
||||||
return (e) => {
|
return e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
status: 'FOCUS'
|
status: 'FOCUS'
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.setValue(storageKey + '-' + folderKey)
|
this.setValue(storageKey + '-' + folderKey)
|
||||||
this.refs.root.focus()
|
this.refs.root.focus()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +216,7 @@ class FolderSelect extends React.Component {
|
|||||||
const folderKey = splitted.shift()
|
const folderKey = splitted.shift()
|
||||||
let options = []
|
let options = []
|
||||||
data.storageMap.forEach((storage, index) => {
|
data.storageMap.forEach((storage, index) => {
|
||||||
storage.folders.forEach((folder) => {
|
storage.folders.forEach(folder => {
|
||||||
options.push({
|
options.push({
|
||||||
storage: storage,
|
storage: storage,
|
||||||
folder: folder
|
folder: folder
|
||||||
@@ -200,39 +224,49 @@ class FolderSelect extends React.Component {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
const currentOption = options.filter(
|
||||||
|
option =>
|
||||||
|
option.storage.key === storageKey && option.folder.key === folderKey
|
||||||
|
)[0]
|
||||||
|
|
||||||
if (this.state.search.trim().length > 0) {
|
if (this.state.search.trim().length > 0) {
|
||||||
const filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
|
const filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
|
||||||
options = options.filter((option) => filter.test(option.folder.name))
|
options = options.filter(option => filter.test(option.folder.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionList = options
|
const optionList = options.map((option, index) => {
|
||||||
.map((option, index) => {
|
|
||||||
return (
|
return (
|
||||||
<div styleName={index === this.state.optionIndex
|
<div
|
||||||
|
styleName={
|
||||||
|
index === this.state.optionIndex
|
||||||
? 'search-optionList-item--active'
|
? 'search-optionList-item--active'
|
||||||
: 'search-optionList-item'
|
: 'search-optionList-item'
|
||||||
}
|
}
|
||||||
key={option.storage.key + '-' + option.folder.key}
|
key={option.storage.key + '-' + option.folder.key}
|
||||||
onClick={(e) => this.handleOptionClick(option.storage.key, option.folder.key)(e)}
|
onClick={e =>
|
||||||
|
this.handleOptionClick(option.storage.key, option.folder.key)(e)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span styleName='search-optionList-item-name'
|
<span
|
||||||
|
styleName='search-optionList-item-name'
|
||||||
style={{ borderColor: option.folder.color }}
|
style={{ borderColor: option.folder.color }}
|
||||||
>
|
>
|
||||||
{option.folder.name}
|
{option.folder.name}
|
||||||
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span>
|
<span styleName='search-optionList-item-name-surfix'>
|
||||||
|
in {option.storage.name}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={_.isString(className)
|
<div
|
||||||
? 'FolderSelect ' + className
|
className={
|
||||||
: 'FolderSelect'
|
_.isString(className) ? 'FolderSelect ' + className : 'FolderSelect'
|
||||||
}
|
}
|
||||||
styleName={this.state.status === 'SEARCH'
|
styleName={
|
||||||
|
this.state.status === 'SEARCH'
|
||||||
? 'root--search'
|
? 'root--search'
|
||||||
: this.state.status === 'FOCUS'
|
: this.state.status === 'FOCUS'
|
||||||
? 'root--focus'
|
? 'root--focus'
|
||||||
@@ -240,28 +274,28 @@ class FolderSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
ref='root'
|
ref='root'
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
onClick={(e) => this.handleClick(e)}
|
onClick={e => this.handleClick(e)}
|
||||||
onFocus={(e) => this.handleFocus(e)}
|
onFocus={e => this.handleFocus(e)}
|
||||||
onBlur={(e) => this.handleBlur(e)}
|
onBlur={e => this.handleBlur(e)}
|
||||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
onKeyDown={e => this.handleKeyDown(e)}
|
||||||
>
|
>
|
||||||
{this.state.status === 'SEARCH'
|
{this.state.status === 'SEARCH' ? (
|
||||||
? <div styleName='search'>
|
<div styleName='search'>
|
||||||
<input styleName='search-input'
|
<input
|
||||||
|
styleName='search-input'
|
||||||
ref='search'
|
ref='search'
|
||||||
value={this.state.search}
|
value={this.state.search}
|
||||||
placeholder={i18n.__('Folder...')}
|
placeholder={i18n.__('Folder...')}
|
||||||
onChange={(e) => this.handleSearchInputChange(e)}
|
onChange={e => this.handleSearchInputChange(e)}
|
||||||
onBlur={(e) => this.handleSearchInputBlur(e)}
|
onBlur={e => this.handleSearchInputBlur(e)}
|
||||||
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
|
onKeyDown={e => this.handleSearchInputKeyDown(e)}
|
||||||
/>
|
/>
|
||||||
<div styleName='search-optionList'
|
<div styleName='search-optionList' ref='optionList'>
|
||||||
ref='optionList'
|
|
||||||
>
|
|
||||||
{optionList}
|
{optionList}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: <div styleName='idle' style={{color: currentOption.folder.color}}>
|
) : (
|
||||||
|
<div styleName='idle' style={{ color: currentOption.folder.color }}>
|
||||||
<div styleName='idle-label'>
|
<div styleName='idle-label'>
|
||||||
<i className='fa fa-folder' />
|
<i className='fa fa-folder' />
|
||||||
<span styleName='idle-label-name'>
|
<span styleName='idle-label-name'>
|
||||||
@@ -269,8 +303,7 @@ class FolderSelect extends React.Component {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -280,11 +313,13 @@ FolderSelect.propTypes = {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
folders: PropTypes.arrayOf(PropTypes.shape({
|
folders: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
key: PropTypes.string,
|
key: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
color: PropTypes.string
|
color: PropTypes.string
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(FolderSelect, styles)
|
export default CSSModules(FolderSelect, styles)
|
||||||
|
|||||||
@@ -36,20 +36,22 @@ class FromUrlButton extends React.Component {
|
|||||||
const { className } = this.props
|
const { className } = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={_.isString(className)
|
<button
|
||||||
? 'FromUrlButton ' + className
|
className={
|
||||||
: 'FromUrlButton'
|
_.isString(className) ? 'FromUrlButton ' + className : 'FromUrlButton'
|
||||||
}
|
}
|
||||||
styleName={this.state.isActive || this.props.isActive
|
styleName={
|
||||||
? 'root--active'
|
this.state.isActive || this.props.isActive ? 'root--active' : 'root'
|
||||||
: 'root'
|
|
||||||
}
|
}
|
||||||
onMouseDown={(e) => this.handleMouseDown(e)}
|
onMouseDown={e => this.handleMouseDown(e)}
|
||||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
onMouseLeave={e => this.handleMouseLeave(e)}
|
||||||
onClick={this.props.onClick}>
|
onClick={this.props.onClick}
|
||||||
<img styleName='icon'
|
>
|
||||||
src={this.state.isActive || this.props.isActive
|
<img
|
||||||
|
styleName='icon'
|
||||||
|
src={
|
||||||
|
this.state.isActive || this.props.isActive
|
||||||
? '../resources/icon/icon-external.svg'
|
? '../resources/icon/icon-external.svg'
|
||||||
: '../resources/icon/icon-external.svg'
|
: '../resources/icon/icon-external.svg'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ import styles from './FullscreenButton.styl'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
const FullscreenButton = ({
|
const FullscreenButton = ({ onClick }) => {
|
||||||
onClick
|
|
||||||
}) => {
|
|
||||||
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
||||||
return (
|
return (
|
||||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
<button
|
||||||
|
styleName='control-fullScreenButton'
|
||||||
|
title={i18n.__('Fullscreen')}
|
||||||
|
onMouseDown={e => onClick(e)}
|
||||||
|
>
|
||||||
<img src='../resources/icon/icon-full.svg' />
|
<img src='../resources/icon/icon-full.svg' />
|
||||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
<span lang={i18n.locale} styleName='tooltip'>
|
||||||
|
{i18n.__('Fullscreen')}({hotkey})
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './InfoButton.styl'
|
import styles from './InfoButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoButton = ({
|
const InfoButton = ({ onClick }) => (
|
||||||
onClick
|
<button styleName='control-infoButton' onClick={e => onClick(e)}>
|
||||||
}) => (
|
|
||||||
<button styleName='control-infoButton'
|
|
||||||
onClick={(e) => onClick(e)}
|
|
||||||
>
|
|
||||||
<img className='infoButton' src='../resources/icon/icon-info.svg' />
|
<img className='infoButton' src='../resources/icon/icon-info.svg' />
|
||||||
<span styleName='tooltip'>{i18n.__('Info')}</span>
|
<span styleName='tooltip'>{i18n.__('Info')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,20 +14,39 @@ class InfoPanel extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf, wordCount, letterCount, type, print
|
storageName,
|
||||||
|
folderName,
|
||||||
|
noteLink,
|
||||||
|
updatedAt,
|
||||||
|
createdAt,
|
||||||
|
exportAsMd,
|
||||||
|
exportAsTxt,
|
||||||
|
exportAsHtml,
|
||||||
|
exportAsPdf,
|
||||||
|
wordCount,
|
||||||
|
letterCount,
|
||||||
|
type,
|
||||||
|
print
|
||||||
} = this.props
|
} = this.props
|
||||||
return (
|
return (
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
<div
|
||||||
|
className='infoPanel'
|
||||||
|
styleName='control-infoButton-panel'
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p styleName='modification-date'>{updatedAt}</p>
|
<p styleName='modification-date'>{updatedAt}</p>
|
||||||
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
<p styleName='modification-date-desc'>
|
||||||
|
{i18n.__('MODIFICATION DATE')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
{type === 'SNIPPET_NOTE'
|
{type === 'SNIPPET_NOTE' ? (
|
||||||
? ''
|
''
|
||||||
: <div styleName='count-wrap'>
|
) : (
|
||||||
|
<div styleName='count-wrap'>
|
||||||
<div styleName='count-number'>
|
<div styleName='count-number'>
|
||||||
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
||||||
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
|
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
|
||||||
@@ -37,12 +56,9 @@ class InfoPanel extends React.Component {
|
|||||||
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
|
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{type === 'SNIPPET_NOTE'
|
{type === 'SNIPPET_NOTE' ? '' : <hr />}
|
||||||
? ''
|
|
||||||
: <hr />
|
|
||||||
}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'>{storageName}</p>
|
<p styleName='infoPanel-default'>{storageName}</p>
|
||||||
@@ -60,8 +76,18 @@ class InfoPanel extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<input styleName='infoPanel-noteLink' ref='noteLink' defaultValue={noteLink} onClick={(e) => { e.target.select() }} />
|
<input
|
||||||
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
styleName='infoPanel-noteLink'
|
||||||
|
ref='noteLink'
|
||||||
|
defaultValue={noteLink}
|
||||||
|
onClick={e => {
|
||||||
|
e.target.select()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => this.copyNoteLink()}
|
||||||
|
styleName='infoPanel-copyButton'
|
||||||
|
>
|
||||||
<i className='fa fa-clipboard' />
|
<i className='fa fa-clipboard' />
|
||||||
</button>
|
</button>
|
||||||
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
|
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
|
||||||
@@ -70,27 +96,39 @@ class InfoPanel extends React.Component {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div id='export-wrap'>
|
<div id='export-wrap'>
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
<button
|
||||||
|
styleName='export--enable'
|
||||||
|
onClick={e => exportAsMd(e, 'export-md')}
|
||||||
|
>
|
||||||
<i className='fa fa-file-code-o' />
|
<i className='fa fa-file-code-o' />
|
||||||
<p>{i18n.__('.md')}</p>
|
<p>{i18n.__('.md')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
<button
|
||||||
|
styleName='export--enable'
|
||||||
|
onClick={e => exportAsTxt(e, 'export-txt')}
|
||||||
|
>
|
||||||
<i className='fa fa-file-text-o' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>{i18n.__('.txt')}</p>
|
<p>{i18n.__('.txt')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
<button
|
||||||
|
styleName='export--enable'
|
||||||
|
onClick={e => exportAsHtml(e, 'export-html')}
|
||||||
|
>
|
||||||
<i className='fa fa-html5' />
|
<i className='fa fa-html5' />
|
||||||
<p>{i18n.__('.html')}</p>
|
<p>{i18n.__('.html')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
|
<button
|
||||||
|
styleName='export--enable'
|
||||||
|
onClick={e => exportAsPdf(e, 'export-pdf')}
|
||||||
|
>
|
||||||
<i className='fa fa-file-pdf-o' />
|
<i className='fa fa-file-pdf-o' />
|
||||||
<p>{i18n.__('.pdf')}</p>
|
<p>{i18n.__('.pdf')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
|
<button styleName='export--enable' onClick={e => print(e, 'print')}>
|
||||||
<i className='fa fa-print' />
|
<i className='fa fa-print' />
|
||||||
<p>{i18n.__('Print')}</p>
|
<p>{i18n.__('Print')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,9 +5,20 @@ import styles from './InfoPanel.styl'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoPanelTrashed = ({
|
const InfoPanelTrashed = ({
|
||||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf
|
storageName,
|
||||||
|
folderName,
|
||||||
|
updatedAt,
|
||||||
|
createdAt,
|
||||||
|
exportAsMd,
|
||||||
|
exportAsTxt,
|
||||||
|
exportAsHtml,
|
||||||
|
exportAsPdf
|
||||||
}) => (
|
}) => (
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
<div
|
||||||
|
className='infoPanel'
|
||||||
|
styleName='control-infoButton-panel-trash'
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p styleName='modification-date'>{updatedAt}</p>
|
<p styleName='modification-date'>{updatedAt}</p>
|
||||||
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
||||||
@@ -21,7 +32,10 @@ const InfoPanelTrashed = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
|
<p styleName='infoPanel-default'>
|
||||||
|
<text styleName='infoPanel-trash'>Trash</text>
|
||||||
|
{folderName}
|
||||||
|
</p>
|
||||||
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
|
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,22 +45,34 @@ const InfoPanelTrashed = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='export-wrap'>
|
<div id='export-wrap'>
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
<button
|
||||||
|
styleName='export--enable'
|
||||||
|
onClick={e => exportAsMd(e, 'export-md')}
|
||||||
|
>
|
||||||
<i className='fa fa-file-code-o' />
|
<i className='fa fa-file-code-o' />
|
||||||
<p>.md</p>
|
<p>.md</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
<button
|
||||||
|
styleName='export--enable'
|
||||||
|
onClick={e => exportAsTxt(e, 'export-txt')}
|
||||||
|
>
|
||||||
<i className='fa fa-file-text-o' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>.txt</p>
|
<p>.txt</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
<button
|
||||||
|
styleName='export--enable'
|
||||||
|
onClick={e => exportAsHtml(e, 'export-html')}
|
||||||
|
>
|
||||||
<i className='fa fa-html5' />
|
<i className='fa fa-html5' />
|
||||||
<p>.html</p>
|
<p>.html</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
|
<button
|
||||||
|
styleName='export--enable'
|
||||||
|
onClick={e => exportAsPdf(e, 'export-pdf')}
|
||||||
|
>
|
||||||
<i className='fa fa-file-pdf-o' />
|
<i className='fa fa-file-pdf-o' />
|
||||||
<p>.pdf</p>
|
<p>.pdf</p>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -39,11 +39,14 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isMovingNote: false,
|
isMovingNote: false,
|
||||||
note: Object.assign({
|
note: Object.assign(
|
||||||
|
{
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
}, props.note),
|
},
|
||||||
|
props.note
|
||||||
|
),
|
||||||
isLockButtonShown: props.config.editor.type !== 'SPLIT',
|
isLockButtonShown: props.config.editor.type !== 'SPLIT',
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
editorType: props.config.editor.type,
|
editorType: props.config.editor.type,
|
||||||
@@ -65,7 +68,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection())
|
ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection())
|
||||||
ee.on('topbar:togglemodebutton', () => {
|
ee.on('topbar:togglemodebutton', () => {
|
||||||
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
const reversedType =
|
||||||
|
this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
||||||
this.handleSwitchMode(reversedType)
|
this.handleSwitchMode(reversedType)
|
||||||
})
|
})
|
||||||
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
|
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
|
||||||
@@ -74,15 +78,19 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const isNewNote = nextProps.note.key !== this.props.note.key
|
const isNewNote = nextProps.note.key !== this.props.note.key
|
||||||
const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length
|
const hasDeletedTags =
|
||||||
|
nextProps.note.tags.length < this.props.note.tags.length
|
||||||
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
|
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
|
||||||
if (this.saveQueue != null) this.saveNow()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
note: Object.assign({ linesHighlighted: [] }, nextProps.note)
|
note: Object.assign({ linesHighlighted: [] }, nextProps.note)
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.refs.content.reload()
|
this.refs.content.reload()
|
||||||
if (this.refs.tags) this.refs.tags.reset()
|
if (this.refs.tags) this.refs.tags.reset()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus content if using blur or double click
|
// Focus content if using blur or double click
|
||||||
@@ -117,7 +125,11 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
note.content = this.refs.content.value
|
note.content = this.refs.content.value
|
||||||
|
|
||||||
let title = findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)
|
let title = findNoteTitle(
|
||||||
|
note.content,
|
||||||
|
this.props.config.editor.enableFrontMatterTitle,
|
||||||
|
this.props.config.editor.frontMatterTitleField
|
||||||
|
)
|
||||||
title = striptags(title)
|
title = striptags(title)
|
||||||
title = markdown.strip(title)
|
title = markdown.strip(title)
|
||||||
note.title = title
|
note.title = title
|
||||||
@@ -144,9 +156,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
clearTimeout(this.saveQueue)
|
clearTimeout(this.saveQueue)
|
||||||
this.saveQueue = null
|
this.saveQueue = null
|
||||||
|
|
||||||
dataApi
|
dataApi.updateNote(note.storage, note.key, this.state.note).then(note => {
|
||||||
.updateNote(note.storage, note.key, this.state.note)
|
|
||||||
.then((note) => {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
@@ -164,46 +174,53 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
dataApi
|
dataApi
|
||||||
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||||
.then((newNote) => {
|
.then(newNote => {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
isMovingNote: true,
|
isMovingNote: true,
|
||||||
note: Object.assign({}, newNote)
|
note: Object.assign({}, newNote)
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
const { dispatch, location } = this.props
|
const { dispatch, location } = this.props
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'MOVE_NOTE',
|
type: 'MOVE_NOTE',
|
||||||
originNote: note,
|
originNote: note,
|
||||||
note: newNote
|
note: newNote
|
||||||
})
|
})
|
||||||
dispatch(replace({
|
dispatch(
|
||||||
|
replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: queryString.stringify({
|
search: queryString.stringify({
|
||||||
key: newNote.key
|
key: newNote.key
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
this.setState({
|
this.setState({
|
||||||
isMovingNote: false
|
isMovingNote: false
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStarButtonClick(e) {
|
handleStarButtonClick(e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
if (!note.isStarred)
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
||||||
|
|
||||||
note.isStarred = !note.isStarred
|
note.isStarred = !note.isStarred
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
note
|
note
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
exportAsFile () {
|
exportAsFile() {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
exportAsMd() {
|
exportAsMd() {
|
||||||
ee.emit('export:save-md')
|
ee.emit('export:save-md')
|
||||||
@@ -231,7 +248,11 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
} else if (e.ctrlKey && e.shiftKey) {
|
} else if (e.ctrlKey && e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.jumpPrevTab()
|
this.jumpPrevTab()
|
||||||
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
|
} else if (
|
||||||
|
!e.ctrlKey &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
e.target === this.refs.description
|
||||||
|
) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
}
|
}
|
||||||
@@ -239,9 +260,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
// I key
|
// I key
|
||||||
case 73:
|
case 73:
|
||||||
{
|
{
|
||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper =
|
||||||
? e.metaKey
|
global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey
|
||||||
: e.ctrlKey
|
|
||||||
if (isSuper) {
|
if (isSuper) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.handleInfoButtonClick(e)
|
this.handleInfoButtonClick(e)
|
||||||
@@ -261,7 +281,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
const { note, dispatch } = this.props
|
const { note, dispatch } = this.props
|
||||||
dataApi
|
dataApi
|
||||||
.deleteNote(note.storage, note.key)
|
.deleteNote(note.storage, note.key)
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
const dispatchHandler = () => {
|
const dispatchHandler = () => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'DELETE_NOTE',
|
type: 'DELETE_NOTE',
|
||||||
@@ -277,11 +297,14 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
if (confirmDeleteNote(confirmDeletion, false)) {
|
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||||
note.isTrashed = true
|
note.isTrashed = true
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
note
|
note
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
ee.emit('list:next')
|
ee.emit('list:next')
|
||||||
}
|
}
|
||||||
@@ -293,13 +316,16 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
note.isTrashed = false
|
note.isTrashed = false
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
note
|
note
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
this.refs.content.reload()
|
this.refs.content.reload()
|
||||||
ee.emit('list:next')
|
ee.emit('list:next')
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFullScreenButton(e) {
|
handleFullScreenButton(e) {
|
||||||
@@ -314,7 +340,9 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToggleLockButton() {
|
getToggleLockButton() {
|
||||||
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
|
return this.state.isLocked
|
||||||
|
? '../resources/icon/icon-lock.svg'
|
||||||
|
: '../resources/icon/icon-unlock.svg'
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteKeyDown(e) {
|
handleDeleteKeyDown(e) {
|
||||||
@@ -341,7 +369,9 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleInfoButtonClick(e) {
|
handleInfoButtonClick(e) {
|
||||||
const infoPanel = document.querySelector('.infoPanel')
|
const infoPanel = document.querySelector('.infoPanel')
|
||||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
if (infoPanel.style)
|
||||||
|
infoPanel.style.display =
|
||||||
|
infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
print(e) {
|
print(e) {
|
||||||
@@ -350,12 +380,15 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleSwitchMode(type) {
|
handleSwitchMode(type) {
|
||||||
// If in split mode, hide the lock button
|
// If in split mode, hide the lock button
|
||||||
this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => {
|
this.setState(
|
||||||
|
{ editorType: type, isLockButtonShown: !(type === 'SPLIT') },
|
||||||
|
() => {
|
||||||
this.focus()
|
this.focus()
|
||||||
const newConfig = Object.assign({}, this.props.config)
|
const newConfig = Object.assign({}, this.props.config)
|
||||||
newConfig.editor.type = type
|
newConfig.editor.type = type
|
||||||
ConfigManager.set(newConfig)
|
ConfigManager.set(newConfig)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSwitchDirection() {
|
handleSwitchDirection() {
|
||||||
@@ -372,14 +405,16 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
const splitted = note.content.split('\n')
|
const splitted = note.content.split('\n')
|
||||||
|
|
||||||
const clearTodoContent = splitted.map((line) => {
|
const clearTodoContent = splitted
|
||||||
|
.map(line => {
|
||||||
const trimmedLine = line.trim()
|
const trimmedLine = line.trim()
|
||||||
if (trimmedLine.match(/\[x\]/i)) {
|
if (trimmedLine.match(/\[x\]/i)) {
|
||||||
return line.replace(/\[x\]/i, '[ ]')
|
return line.replace(/\[x\]/i, '[ ]')
|
||||||
} else {
|
} else {
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
}).join('\n')
|
})
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
note.content = clearTodoContent
|
note.content = clearTodoContent
|
||||||
this.refs.content.setValue(note.content)
|
this.refs.content.setValue(note.content)
|
||||||
@@ -392,7 +427,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||||
return <MarkdownEditor
|
return (
|
||||||
|
<MarkdownEditor
|
||||||
ref='content'
|
ref='content'
|
||||||
styleName='body-noteEditor'
|
styleName='body-noteEditor'
|
||||||
config={config}
|
config={config}
|
||||||
@@ -405,8 +441,10 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
RTL={this.state.RTL}
|
RTL={this.state.RTL}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return <MarkdownSplitEditor
|
return (
|
||||||
|
<MarkdownSplitEditor
|
||||||
ref='content'
|
ref='content'
|
||||||
config={config}
|
config={config}
|
||||||
value={note.content}
|
value={note.content}
|
||||||
@@ -417,6 +455,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
RTL={this.state.RTL}
|
RTL={this.state.RTL}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,24 +467,28 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
const options = []
|
const options = []
|
||||||
data.storageMap.forEach((storage, index) => {
|
data.storageMap.forEach((storage, index) => {
|
||||||
storage.folders.forEach((folder) => {
|
storage.folders.forEach(folder => {
|
||||||
options.push({
|
options.push({
|
||||||
storage: storage,
|
storage: storage,
|
||||||
folder: folder
|
folder: folder
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
const currentOption = options.filter(
|
||||||
|
option =>
|
||||||
|
option.storage.key === storageKey && option.folder.key === folderKey
|
||||||
|
)[0]
|
||||||
|
|
||||||
const trashTopBar = <div styleName='info'>
|
const trashTopBar = (
|
||||||
|
<div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
<RestoreButton onClick={e => this.handleUndoButtonClick(e)} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<PermanentDeleteButton
|
||||||
<InfoButton
|
onClick={e => this.handleTrashButtonClick(e)}
|
||||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
|
||||||
/>
|
/>
|
||||||
|
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
|
||||||
<InfoPanelTrashed
|
<InfoPanelTrashed
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
@@ -458,15 +501,18 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const detailTopBar = <div styleName='info'>
|
const detailTopBar = (
|
||||||
|
<div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<div>
|
<div>
|
||||||
<FolderSelect styleName='info-left-top-folderSelect'
|
<FolderSelect
|
||||||
|
styleName='info-left-top-folderSelect'
|
||||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||||
ref='folder'
|
ref='folder'
|
||||||
data={data}
|
data={data}
|
||||||
onChange={(e) => this.handleFolderChange(e)}
|
onChange={e => this.handleFolderChange(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -480,44 +526,57 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
onChange={this.handleUpdateTag.bind(this)}
|
onChange={this.handleUpdateTag.bind(this)}
|
||||||
coloredTags={config.coloredTags}
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
<TodoListPercentage
|
||||||
|
onClearCheckboxClick={e => this.handleClearTodo(e)}
|
||||||
|
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
<ToggleModeButton
|
||||||
<ToggleDirectionButton onClick={(e) => this.handleSwitchDirection(e)} isRTL={this.state.RTL} />
|
onClick={e => this.handleSwitchMode(e)}
|
||||||
|
editorType={editorType}
|
||||||
|
/>
|
||||||
|
<ToggleDirectionButton
|
||||||
|
onClick={e => this.handleSwitchDirection(e)}
|
||||||
|
isRTL={this.state.RTL}
|
||||||
|
/>
|
||||||
<StarButton
|
<StarButton
|
||||||
onClick={(e) => this.handleStarButtonClick(e)}
|
onClick={e => this.handleStarButtonClick(e)}
|
||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const imgSrc = `${this.getToggleLockButton()}`
|
const imgSrc = `${this.getToggleLockButton()}`
|
||||||
const lockButtonComponent =
|
const lockButtonComponent = (
|
||||||
<button styleName='control-lockButton'
|
<button
|
||||||
onFocus={(e) => this.handleFocus(e)}
|
styleName='control-lockButton'
|
||||||
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
onFocus={e => this.handleFocus(e)}
|
||||||
|
onMouseDown={e => this.handleLockButtonMouseDown(e)}
|
||||||
>
|
>
|
||||||
<img src={imgSrc} />
|
<img src={imgSrc} />
|
||||||
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
|
{this.state.isLocked ? (
|
||||||
|
<span styleName='tooltip'>Unlock</span>
|
||||||
|
) : (
|
||||||
|
<span styleName='tooltip'>Lock</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
return (
|
|
||||||
this.state.isLockButtonShown ? lockButtonComponent : ''
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return this.state.isLockButtonShown ? lockButtonComponent : ''
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
|
<FullscreenButton onClick={e => this.handleFullScreenButton(e)} />
|
||||||
|
|
||||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<TrashButton onClick={e => this.handleTrashButtonClick(e)} />
|
||||||
|
|
||||||
<InfoButton
|
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
|
||||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
|
noteLink={`[${note.title}](:note:${
|
||||||
|
queryString.parse(location.search).key
|
||||||
|
})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.exportAsMd}
|
exportAsMd={this.exportAsMd}
|
||||||
@@ -530,21 +589,19 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
print={this.print}
|
print={this.print}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='NoteDetail'
|
<div
|
||||||
|
className='NoteDetail'
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
styleName='root'
|
styleName='root'
|
||||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
onKeyDown={e => this.handleKeyDown(e)}
|
||||||
>
|
>
|
||||||
|
|
||||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||||
|
|
||||||
<div styleName='body'>
|
<div styleName='body'>{this.renderEditor()}</div>
|
||||||
{this.renderEditor()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<StatusBar
|
<StatusBar
|
||||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||||
@@ -558,9 +615,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
MarkdownNoteDetail.propTypes = {
|
MarkdownNoteDetail.propTypes = {
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
repositories: PropTypes.array,
|
repositories: PropTypes.array,
|
||||||
note: PropTypes.shape({
|
note: PropTypes.shape({}),
|
||||||
|
|
||||||
}),
|
|
||||||
style: PropTypes.shape({
|
style: PropTypes.shape({
|
||||||
left: PropTypes.number
|
left: PropTypes.number
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './TrashButton.styl'
|
import styles from './TrashButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const PermanentDeleteButton = ({
|
const PermanentDeleteButton = ({ onClick }) => (
|
||||||
onClick
|
<button styleName='control-trashButton--in-trash' onClick={e => onClick(e)}>
|
||||||
}) => (
|
|
||||||
<button styleName='control-trashButton--in-trash'
|
|
||||||
onClick={(e) => onClick(e)}
|
|
||||||
>
|
|
||||||
<img src='../resources/icon/icon-trash.svg' />
|
<img src='../resources/icon/icon-trash.svg' />
|
||||||
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './RestoreButton.styl'
|
import styles from './RestoreButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const RestoreButton = ({
|
const RestoreButton = ({ onClick }) => (
|
||||||
onClick
|
<button styleName='control-restoreButton' onClick={onClick}>
|
||||||
}) => (
|
|
||||||
<button styleName='control-restoreButton'
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
|
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
|
||||||
<span styleName='tooltip'>{i18n.__('Restore')}</span>
|
<span styleName='tooltip'>{i18n.__('Restore')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -46,11 +46,17 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
showArrows: false,
|
showArrows: false,
|
||||||
enableLeftArrow: false,
|
enableLeftArrow: false,
|
||||||
enableRightArrow: false,
|
enableRightArrow: false,
|
||||||
note: Object.assign({
|
note: Object.assign(
|
||||||
|
{
|
||||||
description: ''
|
description: ''
|
||||||
}, props.note, {
|
},
|
||||||
snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
|
props.note,
|
||||||
})
|
{
|
||||||
|
snippets: props.note.snippets.map(snippet =>
|
||||||
|
Object.assign({ linesHighlighted: [] }, snippet)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollToNextTabThreshold = 0.7
|
this.scrollToNextTabThreshold = 0.7
|
||||||
@@ -64,7 +70,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
if (visibleTabs.offsetWidth < allTabs.scrollWidth) {
|
if (visibleTabs.offsetWidth < allTabs.scrollWidth) {
|
||||||
this.setState({
|
this.setState({
|
||||||
showArrows: visibleTabs.offsetWidth < allTabs.scrollWidth,
|
showArrows: visibleTabs.offsetWidth < allTabs.scrollWidth,
|
||||||
enableRightArrow: allTabs.offsetLeft !== visibleTabs.offsetWidth - allTabs.scrollWidth,
|
enableRightArrow:
|
||||||
|
allTabs.offsetLeft !== visibleTabs.offsetWidth - allTabs.scrollWidth,
|
||||||
enableLeftArrow: allTabs.offsetLeft !== 0
|
enableLeftArrow: allTabs.offsetLeft !== 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -72,25 +79,37 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
|
if (
|
||||||
|
nextProps.note.key !== this.props.note.key &&
|
||||||
|
!this.state.isMovingNote
|
||||||
|
) {
|
||||||
if (this.saveQueue != null) this.saveNow()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
const nextNote = Object.assign({
|
const nextNote = Object.assign(
|
||||||
|
{
|
||||||
description: ''
|
description: ''
|
||||||
}, nextProps.note, {
|
},
|
||||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
|
nextProps.note,
|
||||||
})
|
{
|
||||||
|
snippets: nextProps.note.snippets.map(snippet =>
|
||||||
|
Object.assign({ linesHighlighted: [] }, snippet)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
snippetIndex: 0,
|
snippetIndex: 0,
|
||||||
note: nextNote
|
note: nextNote
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
const { snippets } = this.state.note
|
const { snippets } = this.state.note
|
||||||
snippets.forEach((snippet, index) => {
|
snippets.forEach((snippet, index) => {
|
||||||
this.refs['code-' + index].reload()
|
this.refs['code-' + index].reload()
|
||||||
})
|
})
|
||||||
if (this.refs.tags) this.refs.tags.reset()
|
if (this.refs.tags) this.refs.tags.reset()
|
||||||
this.setState(this.getArrowsState())
|
this.setState(this.getArrowsState())
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,11 +135,14 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
note.updatedAt = new Date()
|
note.updatedAt = new Date()
|
||||||
note.title = findNoteTitle(note.description, false)
|
note.title = findNoteTitle(note.description, false)
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
note
|
note
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
@@ -135,9 +157,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
clearTimeout(this.saveQueue)
|
clearTimeout(this.saveQueue)
|
||||||
this.saveQueue = null
|
this.saveQueue = null
|
||||||
|
|
||||||
dataApi
|
dataApi.updateNote(note.storage, note.key, this.state.note).then(note => {
|
||||||
.updateNote(note.storage, note.key, this.state.note)
|
|
||||||
.then((note) => {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
@@ -155,46 +175,53 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
dataApi
|
dataApi
|
||||||
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||||
.then((newNote) => {
|
.then(newNote => {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
isMovingNote: true,
|
isMovingNote: true,
|
||||||
note: Object.assign({}, newNote)
|
note: Object.assign({}, newNote)
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
const { dispatch, location } = this.props
|
const { dispatch, location } = this.props
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'MOVE_NOTE',
|
type: 'MOVE_NOTE',
|
||||||
originNote: note,
|
originNote: note,
|
||||||
note: newNote
|
note: newNote
|
||||||
})
|
})
|
||||||
dispatch(replace({
|
dispatch(
|
||||||
|
replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: queryString.stringify({
|
search: queryString.stringify({
|
||||||
key: newNote.key
|
key: newNote.key
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
this.setState({
|
this.setState({
|
||||||
isMovingNote: false
|
isMovingNote: false
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStarButtonClick(e) {
|
handleStarButtonClick(e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
if (!note.isStarred)
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
||||||
|
|
||||||
note.isStarred = !note.isStarred
|
note.isStarred = !note.isStarred
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
note
|
note
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
exportAsFile () {
|
exportAsFile() {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTrashButtonClick(e) {
|
handleTrashButtonClick(e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
@@ -206,7 +233,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const { note, dispatch } = this.props
|
const { note, dispatch } = this.props
|
||||||
dataApi
|
dataApi
|
||||||
.deleteNote(note.storage, note.key)
|
.deleteNote(note.storage, note.key)
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
const dispatchHandler = () => {
|
const dispatchHandler = () => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'DELETE_NOTE',
|
type: 'DELETE_NOTE',
|
||||||
@@ -222,11 +249,14 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
if (confirmDeleteNote(confirmDeletion, false)) {
|
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||||
note.isTrashed = true
|
note.isTrashed = true
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
note
|
note
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
ee.emit('list:next')
|
ee.emit('list:next')
|
||||||
}
|
}
|
||||||
@@ -238,12 +268,15 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
note.isTrashed = false
|
note.isTrashed = false
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
note
|
note
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
ee.emit('list:next')
|
ee.emit('list:next')
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFullScreenButton(e) {
|
handleFullScreenButton(e) {
|
||||||
@@ -255,14 +288,18 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const left = this.visibleTabs.scrollLeft
|
const left = this.visibleTabs.scrollLeft
|
||||||
|
|
||||||
const tabs = this.allTabs.querySelectorAll('div')
|
const tabs = this.allTabs.querySelectorAll('div')
|
||||||
const lastVisibleTab = Array.from(tabs).find((tab) => {
|
const lastVisibleTab = Array.from(tabs).find(tab => {
|
||||||
return tab.offsetLeft + tab.offsetWidth >= left
|
return tab.offsetLeft + tab.offsetWidth >= left
|
||||||
})
|
})
|
||||||
|
|
||||||
if (lastVisibleTab) {
|
if (lastVisibleTab) {
|
||||||
const visiblePart = lastVisibleTab.offsetWidth + lastVisibleTab.offsetLeft - left
|
const visiblePart =
|
||||||
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
lastVisibleTab.offsetWidth + lastVisibleTab.offsetLeft - left
|
||||||
const scrollToTab = (isFullyVisible && lastVisibleTab.previousSibling)
|
const isFullyVisible =
|
||||||
|
visiblePart >
|
||||||
|
lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
||||||
|
const scrollToTab =
|
||||||
|
isFullyVisible && lastVisibleTab.previousSibling
|
||||||
? lastVisibleTab.previousSibling
|
? lastVisibleTab.previousSibling
|
||||||
: lastVisibleTab
|
: lastVisibleTab
|
||||||
|
|
||||||
@@ -278,14 +315,16 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const width = this.visibleTabs.offsetWidth
|
const width = this.visibleTabs.offsetWidth
|
||||||
|
|
||||||
const tabs = this.allTabs.querySelectorAll('div')
|
const tabs = this.allTabs.querySelectorAll('div')
|
||||||
const lastVisibleTab = Array.from(tabs).find((tab) => {
|
const lastVisibleTab = Array.from(tabs).find(tab => {
|
||||||
return tab.offsetLeft + tab.offsetWidth >= width + left
|
return tab.offsetLeft + tab.offsetWidth >= width + left
|
||||||
})
|
})
|
||||||
|
|
||||||
if (lastVisibleTab) {
|
if (lastVisibleTab) {
|
||||||
const visiblePart = width + left - lastVisibleTab.offsetLeft
|
const visiblePart = width + left - lastVisibleTab.offsetLeft
|
||||||
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
const isFullyVisible =
|
||||||
const scrollToTab = (isFullyVisible && lastVisibleTab.nextSibling)
|
visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
||||||
|
const scrollToTab =
|
||||||
|
isFullyVisible && lastVisibleTab.nextSibling
|
||||||
? lastVisibleTab.nextSibling
|
? lastVisibleTab.nextSibling
|
||||||
: lastVisibleTab
|
: lastVisibleTab
|
||||||
|
|
||||||
@@ -348,7 +387,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const snippets = this.state.note.snippets.slice()
|
const snippets = this.state.note.snippets.slice()
|
||||||
snippets.splice(index, 1)
|
snippets.splice(index, 1)
|
||||||
const note = Object.assign({}, this.state.note, { snippets })
|
const note = Object.assign({}, this.state.note, { snippets })
|
||||||
const snippetIndex = this.state.snippetIndex >= snippets.length
|
const snippetIndex =
|
||||||
|
this.state.snippetIndex >= snippets.length
|
||||||
? snippets.length - 1
|
? snippets.length - 1
|
||||||
: this.state.snippetIndex
|
: this.state.snippetIndex
|
||||||
this.setState({ note, snippetIndex }, () => {
|
this.setState({ note, snippetIndex }, () => {
|
||||||
@@ -359,7 +399,10 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.moveTabBarBy(0)
|
this.moveTabBarBy(0)
|
||||||
} else {
|
} else {
|
||||||
const lastTab = this.allTabs.lastChild
|
const lastTab = this.allTabs.lastChild
|
||||||
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
|
if (
|
||||||
|
lastTab.offsetLeft + lastTab.offsetWidth <
|
||||||
|
this.visibleTabs.offsetWidth
|
||||||
|
) {
|
||||||
const width = this.visibleTabs.offsetWidth
|
const width = this.visibleTabs.offsetWidth
|
||||||
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
||||||
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
||||||
@@ -381,26 +424,36 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
name: mode
|
name: mode
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
|
||||||
|
|
||||||
this.setState(state => ({
|
this.setState(state => ({
|
||||||
|
note: Object.assign(state.note, { snippets: snippets })
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
state => ({
|
||||||
note: state.note
|
note: state.note
|
||||||
}), () => {
|
}),
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModeOptionClick(index, name) {
|
handleModeOptionClick(index, name) {
|
||||||
return (e) => {
|
return e => {
|
||||||
const snippets = this.state.note.snippets.slice()
|
const snippets = this.state.note.snippets.slice()
|
||||||
snippets[index].mode = name
|
snippets[index].mode = name
|
||||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
|
||||||
|
|
||||||
this.setState(state => ({
|
this.setState(state => ({
|
||||||
|
note: Object.assign(state.note, { snippets: snippets })
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
state => ({
|
||||||
note: state.note
|
note: state.note
|
||||||
}), () => {
|
}),
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('SELECT_LANG', {
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('SELECT_LANG', {
|
||||||
name
|
name
|
||||||
@@ -409,17 +462,22 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleCodeChange(index) {
|
handleCodeChange(index) {
|
||||||
return (e) => {
|
return e => {
|
||||||
const snippets = this.state.note.snippets.slice()
|
const snippets = this.state.note.snippets.slice()
|
||||||
snippets[index].content = this.refs['code-' + index].value
|
snippets[index].content = this.refs['code-' + index].value
|
||||||
snippets[index].linesHighlighted = e.options.linesHighlighted
|
snippets[index].linesHighlighted = e.options.linesHighlighted
|
||||||
|
|
||||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
|
||||||
this.setState(state => ({
|
this.setState(state => ({
|
||||||
|
note: Object.assign(state.note, { snippets: snippets })
|
||||||
|
}))
|
||||||
|
this.setState(
|
||||||
|
state => ({
|
||||||
note: state.note
|
note: state.note
|
||||||
}), () => {
|
}),
|
||||||
|
() => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +491,11 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
} else if (e.ctrlKey && e.shiftKey) {
|
} else if (e.ctrlKey && e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.jumpPrevTab()
|
this.jumpPrevTab()
|
||||||
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
|
} else if (
|
||||||
|
!e.ctrlKey &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
e.target === this.refs.description
|
||||||
|
) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
}
|
}
|
||||||
@@ -441,9 +503,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
// I key
|
// I key
|
||||||
case 73:
|
case 73:
|
||||||
{
|
{
|
||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper =
|
||||||
? e.metaKey
|
global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey
|
||||||
: e.ctrlKey
|
|
||||||
if (isSuper) {
|
if (isSuper) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.handleInfoButtonClick(e)
|
this.handleInfoButtonClick(e)
|
||||||
@@ -453,9 +514,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
// L key
|
// L key
|
||||||
case 76:
|
case 76:
|
||||||
{
|
{
|
||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper =
|
||||||
? e.metaKey
|
global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey
|
||||||
: e.ctrlKey
|
|
||||||
if (isSuper) {
|
if (isSuper) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.focus()
|
this.focus()
|
||||||
@@ -465,9 +525,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
// T key
|
// T key
|
||||||
case 84:
|
case 84:
|
||||||
{
|
{
|
||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper =
|
||||||
? e.metaKey
|
global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey
|
||||||
: e.ctrlKey
|
|
||||||
if (isSuper && !e.shiftKey && !e.altKey) {
|
if (isSuper && !e.shiftKey && !e.altKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.addSnippet()
|
this.addSnippet()
|
||||||
@@ -479,10 +538,14 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleModeButtonClick(e, index) {
|
handleModeButtonClick(e, index) {
|
||||||
const templetes = []
|
const templetes = []
|
||||||
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
|
CodeMirror.modeInfo
|
||||||
|
.sort(function(a, b) {
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
.forEach(mode => {
|
||||||
templetes.push({
|
templetes.push({
|
||||||
label: mode.name,
|
label: mode.name,
|
||||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
click: e => this.handleModeOptionClick(index, mode.name)(e)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
context.popup(templetes)
|
context.popup(templetes)
|
||||||
@@ -492,11 +555,11 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
context.popup([
|
context.popup([
|
||||||
{
|
{
|
||||||
label: 'tab',
|
label: 'tab',
|
||||||
click: (e) => this.handleIndentTypeItemClick(e, 'tab')
|
click: e => this.handleIndentTypeItemClick(e, 'tab')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'space',
|
label: 'space',
|
||||||
click: (e) => this.handleIndentTypeItemClick(e, 'space')
|
click: e => this.handleIndentTypeItemClick(e, 'space')
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -505,15 +568,15 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
context.popup([
|
context.popup([
|
||||||
{
|
{
|
||||||
label: '2',
|
label: '2',
|
||||||
click: (e) => this.handleIndentSizeItemClick(e, 2)
|
click: e => this.handleIndentSizeItemClick(e, 2)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '4',
|
label: '4',
|
||||||
click: (e) => this.handleIndentSizeItemClick(e, 4)
|
click: e => this.handleIndentSizeItemClick(e, 4)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '8',
|
label: '8',
|
||||||
click: (e) => this.handleIndentSizeItemClick(e, 8)
|
click: e => this.handleIndentSizeItemClick(e, 8)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -522,11 +585,11 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
context.popup([
|
context.popup([
|
||||||
{
|
{
|
||||||
label: 'on',
|
label: 'on',
|
||||||
click: (e) => this.handleWrapLineItemClick(e, true)
|
click: e => this.handleWrapLineItemClick(e, true)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'off',
|
label: 'off',
|
||||||
click: (e) => this.handleWrapLineItemClick(e, false)
|
click: e => this.handleWrapLineItemClick(e, false)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -584,12 +647,12 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
moveToTab(tab) {
|
moveToTab(tab) {
|
||||||
const easeOutCubic = t => (--t) * t * t + 1
|
const easeOutCubic = t => --t * t * t + 1
|
||||||
const startScrollPosition = this.visibleTabs.scrollLeft
|
const startScrollPosition = this.visibleTabs.scrollLeft
|
||||||
const animationTiming = 300
|
const animationTiming = 300
|
||||||
const scrollMoreCoeff = 1.4 // introduce coefficient, because we want to scroll a bit further to see next tab
|
const scrollMoreCoeff = 1.4 // introduce coefficient, because we want to scroll a bit further to see next tab
|
||||||
|
|
||||||
let scrollBy = (tab.offsetLeft - startScrollPosition)
|
let scrollBy = tab.offsetLeft - startScrollPosition
|
||||||
|
|
||||||
if (tab.offsetLeft > startScrollPosition) {
|
if (tab.offsetLeft > startScrollPosition) {
|
||||||
// if tab is on the right side and we want to show the whole tab in visible area,
|
// if tab is on the right side and we want to show the whole tab in visible area,
|
||||||
@@ -598,7 +661,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
// |____|_______|________|________|_show_this_|
|
// |____|_______|________|________|_show_this_|
|
||||||
// ↑_____________________↑
|
// ↑_____________________↑
|
||||||
// visible area
|
// visible area
|
||||||
scrollBy += (tab.offsetWidth - this.visibleTabs.offsetWidth)
|
scrollBy += tab.offsetWidth - this.visibleTabs.offsetWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
let startTime = null
|
let startTime = null
|
||||||
@@ -606,7 +669,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
startTime = startTime || time
|
startTime = startTime || time
|
||||||
const elapsed = (time - startTime) / animationTiming
|
const elapsed = (time - startTime) / animationTiming
|
||||||
|
|
||||||
this.visibleTabs.scrollLeft = startScrollPosition + easeOutCubic(elapsed) * scrollBy * scrollMoreCoeff
|
this.visibleTabs.scrollLeft =
|
||||||
|
startScrollPosition + easeOutCubic(elapsed) * scrollBy * scrollMoreCoeff
|
||||||
if (elapsed < 1) {
|
if (elapsed < 1) {
|
||||||
window.requestAnimationFrame(scrollAnimation)
|
window.requestAnimationFrame(scrollAnimation)
|
||||||
} else {
|
} else {
|
||||||
@@ -622,30 +686,43 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const visibleTabs = this.visibleTabs
|
const visibleTabs = this.visibleTabs
|
||||||
|
|
||||||
const showArrows = visibleTabs.offsetWidth < allTabs.scrollWidth
|
const showArrows = visibleTabs.offsetWidth < allTabs.scrollWidth
|
||||||
const enableRightArrow = visibleTabs.scrollLeft !== allTabs.scrollWidth - visibleTabs.offsetWidth
|
const enableRightArrow =
|
||||||
|
visibleTabs.scrollLeft !== allTabs.scrollWidth - visibleTabs.offsetWidth
|
||||||
const enableLeftArrow = visibleTabs.scrollLeft !== 0
|
const enableLeftArrow = visibleTabs.scrollLeft !== 0
|
||||||
|
|
||||||
return { showArrows, enableRightArrow, enableLeftArrow }
|
return { showArrows, enableRightArrow, enableLeftArrow }
|
||||||
}
|
}
|
||||||
|
|
||||||
addSnippet() {
|
addSnippet() {
|
||||||
const { config: { editor: { snippetDefaultLanguage } } } = this.props
|
const {
|
||||||
|
config: {
|
||||||
|
editor: { snippetDefaultLanguage }
|
||||||
|
}
|
||||||
|
} = this.props
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
const defaultLanguage = snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
|
const defaultLanguage =
|
||||||
|
snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
|
||||||
|
|
||||||
note.snippets = note.snippets.concat([{
|
note.snippets = note.snippets.concat([
|
||||||
|
{
|
||||||
name: '',
|
name: '',
|
||||||
mode: defaultLanguage,
|
mode: defaultLanguage,
|
||||||
content: '',
|
content: '',
|
||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
}])
|
}
|
||||||
|
])
|
||||||
const snippetIndex = note.snippets.length - 1
|
const snippetIndex = note.snippets.length - 1
|
||||||
|
|
||||||
this.setState(Object.assign({
|
this.setState(
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
note,
|
note,
|
||||||
snippetIndex
|
snippetIndex
|
||||||
}, this.getArrowsState()), () => {
|
},
|
||||||
|
this.getArrowsState()
|
||||||
|
),
|
||||||
|
() => {
|
||||||
if (this.state.showArrows) {
|
if (this.state.showArrows) {
|
||||||
const tabs = this.allTabs.querySelectorAll('div')
|
const tabs = this.allTabs.querySelectorAll('div')
|
||||||
if (tabs) {
|
if (tabs) {
|
||||||
@@ -653,23 +730,32 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.refs['tab-' + snippetIndex].startRenaming()
|
this.refs['tab-' + snippetIndex].startRenaming()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
jumpNextTab() {
|
jumpNextTab() {
|
||||||
this.setState(state => ({
|
this.setState(
|
||||||
|
state => ({
|
||||||
snippetIndex: (state.snippetIndex + 1) % state.note.snippets.length
|
snippetIndex: (state.snippetIndex + 1) % state.note.snippets.length
|
||||||
}), () => {
|
}),
|
||||||
|
() => {
|
||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
jumpPrevTab() {
|
jumpPrevTab() {
|
||||||
this.setState(state => ({
|
this.setState(
|
||||||
snippetIndex: (state.snippetIndex - 1 + state.note.snippets.length) % state.note.snippets.length
|
state => ({
|
||||||
}), () => {
|
snippetIndex:
|
||||||
|
(state.snippetIndex - 1 + state.note.snippets.length) %
|
||||||
|
state.note.snippets.length
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
focusEditor() {
|
focusEditor() {
|
||||||
@@ -678,22 +764,27 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleInfoButtonClick(e) {
|
handleInfoButtonClick(e) {
|
||||||
const infoPanel = document.querySelector('.infoPanel')
|
const infoPanel = document.querySelector('.infoPanel')
|
||||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
if (infoPanel.style)
|
||||||
|
infoPanel.style.display =
|
||||||
|
infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
showWarning(e, msg) {
|
showWarning(e, msg) {
|
||||||
const warningMessage = (msg) => ({
|
const warningMessage = msg =>
|
||||||
|
({
|
||||||
'export-txt': 'Text export',
|
'export-txt': 'Text export',
|
||||||
'export-md': 'Markdown export',
|
'export-md': 'Markdown export',
|
||||||
'export-html': 'HTML export',
|
'export-html': 'HTML export',
|
||||||
'export-pdf': 'PDF export',
|
'export-pdf': 'PDF export',
|
||||||
'print': 'Print'
|
print: 'Print'
|
||||||
})[msg]
|
}[msg])
|
||||||
|
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Sorry!'),
|
message: i18n.__('Sorry!'),
|
||||||
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
detail: i18n.__(
|
||||||
|
warningMessage(msg) + ' is available only in markdown notes.'
|
||||||
|
),
|
||||||
buttons: [i18n.__('OK')]
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -715,38 +806,49 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const tabList = note.snippets.map((snippet, index) => {
|
const tabList = note.snippets.map((snippet, index) => {
|
||||||
const isActive = this.state.snippetIndex === index
|
const isActive = this.state.snippetIndex === index
|
||||||
|
|
||||||
return <SnippetTab
|
return (
|
||||||
|
<SnippetTab
|
||||||
key={index}
|
key={index}
|
||||||
ref={'tab-' + index}
|
ref={'tab-' + index}
|
||||||
snippet={snippet}
|
snippet={snippet}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
onClick={(e) => this.handleTabButtonClick(e, index)}
|
onClick={e => this.handleTabButtonClick(e, index)}
|
||||||
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
|
onDelete={e => this.handleTabDeleteButtonClick(e, index)}
|
||||||
onRename={(name) => this.renameSnippetByIndex(index, name)}
|
onRename={name => this.renameSnippetByIndex(index, name)}
|
||||||
isDeletable={note.snippets.length > 1}
|
isDeletable={note.snippets.length > 1}
|
||||||
onDragStart={(e) => this.handleTabDragStart(e, index)}
|
onDragStart={e => this.handleTabDragStart(e, index)}
|
||||||
onDrop={(e) => this.handleTabDrop(e, index)}
|
onDrop={e => this.handleTabDrop(e, index)}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const viewList = note.snippets.map((snippet, index) => {
|
const viewList = note.snippets.map((snippet, index) => {
|
||||||
const isActive = this.state.snippetIndex === index
|
const isActive = this.state.snippetIndex === index
|
||||||
return <div styleName='tabView'
|
return (
|
||||||
|
<div
|
||||||
|
styleName='tabView'
|
||||||
key={index}
|
key={index}
|
||||||
style={{ zIndex: isActive ? 5 : 4 }}
|
style={{ zIndex: isActive ? 5 : 4 }}
|
||||||
>
|
>
|
||||||
{snippet.mode === 'Markdown' || snippet.mode === 'GitHub Flavored Markdown'
|
{snippet.mode === 'Markdown' ||
|
||||||
? <MarkdownEditor styleName='tabView-content'
|
snippet.mode === 'GitHub Flavored Markdown' ? (
|
||||||
|
<MarkdownEditor
|
||||||
|
styleName='tabView-content'
|
||||||
value={snippet.content}
|
value={snippet.content}
|
||||||
config={config}
|
config={config}
|
||||||
linesHighlighted={snippet.linesHighlighted}
|
linesHighlighted={snippet.linesHighlighted}
|
||||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
onChange={e => this.handleCodeChange(index)(e)}
|
||||||
ref={'code-' + index}
|
ref={'code-' + index}
|
||||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
/>
|
/>
|
||||||
: <CodeEditor styleName='tabView-content'
|
) : (
|
||||||
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
|
<CodeEditor
|
||||||
|
styleName='tabView-content'
|
||||||
|
mode={
|
||||||
|
snippet.mode ||
|
||||||
|
(autoDetect ? null : config.editor.snippetDefaultLanguage)
|
||||||
|
}
|
||||||
value={snippet.content}
|
value={snippet.content}
|
||||||
linesHighlighted={snippet.linesHighlighted}
|
linesHighlighted={snippet.linesHighlighted}
|
||||||
lineWrapping={config.editor.lineWrapping}
|
lineWrapping={config.editor.lineWrapping}
|
||||||
@@ -763,36 +865,41 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
enableTableEditor={config.editor.enableTableEditor}
|
enableTableEditor={config.editor.enableTableEditor}
|
||||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
onChange={e => this.handleCodeChange(index)(e)}
|
||||||
ref={'code-' + index}
|
ref={'code-' + index}
|
||||||
enableSmartPaste={config.editor.enableSmartPaste}
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
hotkey={config.hotkey}
|
hotkey={config.hotkey}
|
||||||
autoDetect={autoDetect}
|
autoDetect={autoDetect}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = []
|
const options = []
|
||||||
data.storageMap.forEach((storage, index) => {
|
data.storageMap.forEach((storage, index) => {
|
||||||
storage.folders.forEach((folder) => {
|
storage.folders.forEach(folder => {
|
||||||
options.push({
|
options.push({
|
||||||
storage: storage,
|
storage: storage,
|
||||||
folder: folder
|
folder: folder
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
const currentOption = options.filter(
|
||||||
|
option =>
|
||||||
|
option.storage.key === storageKey && option.folder.key === folderKey
|
||||||
|
)[0]
|
||||||
|
|
||||||
const trashTopBar = <div styleName='info'>
|
const trashTopBar = (
|
||||||
|
<div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
<RestoreButton onClick={e => this.handleUndoButtonClick(e)} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<PermanentDeleteButton
|
||||||
<InfoButton
|
onClick={e => this.handleTrashButtonClick(e)}
|
||||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
|
||||||
/>
|
/>
|
||||||
|
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
|
||||||
<InfoPanelTrashed
|
<InfoPanelTrashed
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
@@ -805,15 +912,18 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const detailTopBar = <div styleName='info'>
|
const detailTopBar = (
|
||||||
|
<div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<div>
|
<div>
|
||||||
<FolderSelect styleName='info-left-top-folderSelect'
|
<FolderSelect
|
||||||
|
styleName='info-left-top-folderSelect'
|
||||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||||
ref='folder'
|
ref='folder'
|
||||||
data={data}
|
data={data}
|
||||||
onChange={(e) => this.handleFolderChange(e)}
|
onChange={e => this.handleFolderChange(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -824,28 +934,28 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||||
data={data}
|
data={data}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={e => this.handleChange(e)}
|
||||||
coloredTags={config.coloredTags}
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<StarButton
|
<StarButton
|
||||||
onClick={(e) => this.handleStarButtonClick(e)}
|
onClick={e => this.handleStarButtonClick(e)}
|
||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
|
<FullscreenButton onClick={e => this.handleFullScreenButton(e)} />
|
||||||
|
|
||||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<TrashButton onClick={e => this.handleTrashButtonClick(e)} />
|
||||||
|
|
||||||
<InfoButton
|
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
|
||||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
|
noteLink={`[${note.title}](:note:${
|
||||||
|
queryString.parse(location.search).key
|
||||||
|
})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
@@ -857,12 +967,14 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='NoteDetail'
|
<div
|
||||||
|
className='NoteDetail'
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
styleName='root'
|
styleName='root'
|
||||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
onKeyDown={e => this.handleKeyDown(e)}
|
||||||
>
|
>
|
||||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||||
|
|
||||||
@@ -876,31 +988,47 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
ref='description'
|
ref='description'
|
||||||
placeholder={i18n.__('Description...')}
|
placeholder={i18n.__('Description...')}
|
||||||
value={this.state.note.description}
|
value={this.state.note.description}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={e => this.handleChange(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='tabList'>
|
<div styleName='tabList'>
|
||||||
<button styleName='tabButton'
|
<button
|
||||||
|
styleName='tabButton'
|
||||||
hidden={!this.state.showArrows}
|
hidden={!this.state.showArrows}
|
||||||
disabled={!this.state.enableLeftArrow}
|
disabled={!this.state.enableLeftArrow}
|
||||||
onClick={(e) => this.handleTabMoveLeftButtonClick(e)}
|
onClick={e => this.handleTabMoveLeftButtonClick(e)}
|
||||||
>
|
>
|
||||||
<i className='fa fa-chevron-left' />
|
<i className='fa fa-chevron-left' />
|
||||||
</button>
|
</button>
|
||||||
<div styleName='list' onScroll={(e) => { this.setState(this.getArrowsState()) }} ref={(tabs) => { this.visibleTabs = tabs }}>
|
<div
|
||||||
<div styleName='allTabs' ref={(tabs) => { this.allTabs = tabs }}>
|
styleName='list'
|
||||||
|
onScroll={e => {
|
||||||
|
this.setState(this.getArrowsState())
|
||||||
|
}}
|
||||||
|
ref={tabs => {
|
||||||
|
this.visibleTabs = tabs
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
styleName='allTabs'
|
||||||
|
ref={tabs => {
|
||||||
|
this.allTabs = tabs
|
||||||
|
}}
|
||||||
|
>
|
||||||
{tabList}
|
{tabList}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button styleName='tabButton'
|
<button
|
||||||
|
styleName='tabButton'
|
||||||
hidden={!this.state.showArrows}
|
hidden={!this.state.showArrows}
|
||||||
disabled={!this.state.enableRightArrow}
|
disabled={!this.state.enableRightArrow}
|
||||||
onClick={(e) => this.handleTabMoveRightButtonClick(e)}
|
onClick={e => this.handleTabMoveRightButtonClick(e)}
|
||||||
>
|
>
|
||||||
<i className='fa fa-chevron-right' />
|
<i className='fa fa-chevron-right' />
|
||||||
</button>
|
</button>
|
||||||
<button styleName='tabButton'
|
<button
|
||||||
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
styleName='tabButton'
|
||||||
|
onClick={e => this.handleTabPlusButtonClick(e)}
|
||||||
>
|
>
|
||||||
<i className='fa fa-plus' />
|
<i className='fa fa-plus' />
|
||||||
</button>
|
</button>
|
||||||
@@ -910,29 +1038,25 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
<div styleName='override'>
|
<div styleName='override'>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
onClick={e =>
|
||||||
|
this.handleModeButtonClick(e, this.state.snippetIndex)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
||||||
? i18n.__('Select Syntax...')
|
? i18n.__('Select Syntax...')
|
||||||
: this.state.note.snippets[this.state.snippetIndex].mode
|
: this.state.note.snippets[this.state.snippetIndex].mode}
|
||||||
}
|
|
||||||
<i className='fa fa-caret-down' />
|
<i className='fa fa-caret-down' />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={e => this.handleIndentTypeButtonClick(e)}>
|
||||||
onClick={(e) => this.handleIndentTypeButtonClick(e)}
|
|
||||||
>
|
|
||||||
Indent: {config.editor.indentType}
|
Indent: {config.editor.indentType}
|
||||||
<i className='fa fa-caret-down' />
|
<i className='fa fa-caret-down' />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={e => this.handleIndentSizeButtonClick(e)}>
|
||||||
onClick={(e) => this.handleIndentSizeButtonClick(e)}
|
|
||||||
>
|
|
||||||
size: {config.editor.indentSize}
|
size: {config.editor.indentSize}
|
||||||
<i className='fa fa-caret-down' />
|
<i className='fa fa-caret-down' />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={e => this.handleWrapLineButtonClick(e)}>
|
||||||
onClick={(e) => this.handleWrapLineButtonClick(e)}
|
|
||||||
>
|
|
||||||
Wrap Line: {config.editor.lineWrapping ? 'on' : 'off'}
|
Wrap Line: {config.editor.lineWrapping ? 'on' : 'off'}
|
||||||
<i className='fa fa-caret-down' />
|
<i className='fa fa-caret-down' />
|
||||||
</button>
|
</button>
|
||||||
@@ -950,9 +1074,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
SnippetNoteDetail.propTypes = {
|
SnippetNoteDetail.propTypes = {
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
repositories: PropTypes.array,
|
repositories: PropTypes.array,
|
||||||
note: PropTypes.shape({
|
note: PropTypes.shape({}),
|
||||||
|
|
||||||
}),
|
|
||||||
style: PropTypes.shape({
|
style: PropTypes.shape({
|
||||||
left: PropTypes.number
|
left: PropTypes.number
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -36,25 +36,29 @@ class StarButton extends React.Component {
|
|||||||
const { className } = this.props
|
const { className } = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={_.isString(className)
|
<button
|
||||||
? 'StarButton ' + className
|
className={
|
||||||
: 'StarButton'
|
_.isString(className) ? 'StarButton ' + className : 'StarButton'
|
||||||
}
|
}
|
||||||
styleName={this.state.isActive || this.props.isActive
|
styleName={
|
||||||
? 'root--active'
|
this.state.isActive || this.props.isActive ? 'root--active' : 'root'
|
||||||
: 'root'
|
|
||||||
}
|
}
|
||||||
onMouseDown={(e) => this.handleMouseDown(e)}
|
onMouseDown={e => this.handleMouseDown(e)}
|
||||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
onMouseLeave={e => this.handleMouseLeave(e)}
|
||||||
onClick={this.props.onClick}>
|
onClick={this.props.onClick}
|
||||||
<img styleName='icon'
|
>
|
||||||
src={this.state.isActive || this.props.isActive
|
<img
|
||||||
|
styleName='icon'
|
||||||
|
src={
|
||||||
|
this.state.isActive || this.props.isActive
|
||||||
? '../resources/icon/icon-starred.svg'
|
? '../resources/icon/icon-starred.svg'
|
||||||
: '../resources/icon/icon-star.svg'
|
: '../resources/icon/icon-star.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Star')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>
|
||||||
|
{i18n.__('Star')}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ class TagSelect extends React.Component {
|
|||||||
this.onInputBlur = this.onInputBlur.bind(this)
|
this.onInputBlur = this.onInputBlur.bind(this)
|
||||||
this.onInputChange = this.onInputChange.bind(this)
|
this.onInputChange = this.onInputChange.bind(this)
|
||||||
this.onInputKeyDown = this.onInputKeyDown.bind(this)
|
this.onInputKeyDown = this.onInputKeyDown.bind(this)
|
||||||
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
|
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(
|
||||||
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
|
this
|
||||||
|
)
|
||||||
|
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(
|
||||||
|
this
|
||||||
|
)
|
||||||
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
|
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,9 +48,7 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { value } = this.props
|
let { value } = this.props
|
||||||
value = _.isArray(value)
|
value = _.isArray(value) ? value.slice() : []
|
||||||
? value.slice()
|
|
||||||
: []
|
|
||||||
|
|
||||||
if (!_.includes(value, newTag)) {
|
if (!_.includes(value, newTag)) {
|
||||||
value.push(newTag)
|
value.push(newTag)
|
||||||
@@ -56,24 +58,28 @@ class TagSelect extends React.Component {
|
|||||||
value = _.sortBy(value)
|
value = _.sortBy(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
newTag: ''
|
newTag: ''
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
this.value = value
|
this.value = value
|
||||||
this.props.onChange()
|
this.props.onChange()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSuggestions() {
|
buildSuggestions() {
|
||||||
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
|
this.suggestions = _.sortBy(
|
||||||
(tag, name) => ({
|
this.props.data.tagNoteMap
|
||||||
|
.map((tag, name) => ({
|
||||||
name,
|
name,
|
||||||
nameLC: name.toLowerCase(),
|
nameLC: name.toLowerCase(),
|
||||||
size: tag.size
|
size: tag.size
|
||||||
})
|
}))
|
||||||
).filter(
|
.filter(tag => tag.size > 0),
|
||||||
tag => tag.size > 0
|
['name']
|
||||||
), ['name'])
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -146,7 +152,8 @@ class TagSelect extends React.Component {
|
|||||||
const valueLC = value.toLowerCase()
|
const valueLC = value.toLowerCase()
|
||||||
const suggestions = _.filter(
|
const suggestions = _.filter(
|
||||||
this.suggestions,
|
this.suggestions,
|
||||||
tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
|
tag =>
|
||||||
|
!_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
|
||||||
)
|
)
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -159,7 +166,7 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeLastTag() {
|
removeLastTag() {
|
||||||
this.removeTagByCallback((value) => {
|
this.removeTagByCallback(value => {
|
||||||
value.pop()
|
value.pop()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -167,9 +174,7 @@ class TagSelect extends React.Component {
|
|||||||
removeTagByCallback(callback, tag = null) {
|
removeTagByCallback(callback, tag = null) {
|
||||||
let { value } = this.props
|
let { value } = this.props
|
||||||
|
|
||||||
value = _.isArray(value)
|
value = _.isArray(value) ? value.slice() : []
|
||||||
? value.slice()
|
|
||||||
: []
|
|
||||||
callback(value, tag)
|
callback(value, tag)
|
||||||
value = _.uniq(value)
|
value = _.uniq(value)
|
||||||
|
|
||||||
@@ -193,13 +198,14 @@ class TagSelect extends React.Component {
|
|||||||
const { value, className, showTagsAlphabetically, coloredTags } = this.props
|
const { value, className, showTagsAlphabetically, coloredTags } = this.props
|
||||||
|
|
||||||
const tagList = _.isArray(value)
|
const tagList = _.isArray(value)
|
||||||
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
|
? (showTagsAlphabetically ? _.sortBy(value) : value).map(tag => {
|
||||||
const wrapperStyle = {}
|
const wrapperStyle = {}
|
||||||
const textStyle = {}
|
const textStyle = {}
|
||||||
const BLACK = '#333333'
|
const BLACK = '#333333'
|
||||||
const WHITE = '#f1f1f1'
|
const WHITE = '#f1f1f1'
|
||||||
const color = coloredTags[tag]
|
const color = coloredTags[tag]
|
||||||
const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE })
|
const invertedColor =
|
||||||
|
color && invertColor(color, { black: BLACK, white: WHITE })
|
||||||
let iconRemove = '../resources/icon/icon-x.svg'
|
let iconRemove = '../resources/icon/icon-x.svg'
|
||||||
if (color) {
|
if (color) {
|
||||||
wrapperStyle.backgroundColor = color
|
wrapperStyle.backgroundColor = color
|
||||||
@@ -209,15 +215,23 @@ class TagSelect extends React.Component {
|
|||||||
iconRemove = '../resources/icon/icon-x-light.svg'
|
iconRemove = '../resources/icon/icon-x-light.svg'
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span styleName='tag'
|
<span styleName='tag' key={tag} style={wrapperStyle}>
|
||||||
key={tag}
|
<span
|
||||||
style={wrapperStyle}
|
styleName='tag-label'
|
||||||
|
style={textStyle}
|
||||||
|
onClick={e => this.handleTagLabelClick(tag)}
|
||||||
>
|
>
|
||||||
<span styleName='tag-label' style={textStyle} onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
|
#{tag}
|
||||||
<button styleName='tag-removeButton'
|
</span>
|
||||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
|
<button
|
||||||
|
styleName='tag-removeButton'
|
||||||
|
onClick={e => this.handleTagRemoveButtonClick(tag)}
|
||||||
>
|
>
|
||||||
<img className='tag-removeButton-icon' src={iconRemove} width='8px' />
|
<img
|
||||||
|
className='tag-removeButton-icon'
|
||||||
|
src={iconRemove}
|
||||||
|
width='8px'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -227,9 +241,9 @@ class TagSelect extends React.Component {
|
|||||||
const { newTag, suggestions } = this.state
|
const { newTag, suggestions } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={_.isString(className)
|
<div
|
||||||
? 'TagSelect ' + className
|
className={
|
||||||
: 'TagSelect'
|
_.isString(className) ? 'TagSelect ' + className : 'TagSelect'
|
||||||
}
|
}
|
||||||
styleName='root'
|
styleName='root'
|
||||||
>
|
>
|
||||||
@@ -241,11 +255,7 @@ class TagSelect extends React.Component {
|
|||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
getSuggestionValue={suggestion => suggestion.name}
|
getSuggestionValue={suggestion => suggestion.name}
|
||||||
renderSuggestion={suggestion => (
|
renderSuggestion={suggestion => <div>{suggestion.name}</div>}
|
||||||
<div>
|
|
||||||
{suggestion.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
inputProps={{
|
inputProps={{
|
||||||
placeholder: i18n.__('Add tag...'),
|
placeholder: i18n.__('Add tag...'),
|
||||||
value: newTag,
|
value: newTag,
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './ToggleDirectionButton.styl'
|
import styles from './ToggleDirectionButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const ToggleDirectionButton = ({
|
const ToggleDirectionButton = ({ onClick, isRTL }) => (
|
||||||
onClick, isRTL
|
|
||||||
}) => (
|
|
||||||
<div styleName='control-toggleModeButton'>
|
<div styleName='control-toggleModeButton'>
|
||||||
<div styleName={isRTL ? 'active' : undefined} onClick={() => onClick()}>
|
<div styleName={isRTL ? 'active' : undefined} onClick={() => onClick()}>
|
||||||
<img src={!isRTL ? '../resources/icon/icon-left-to-right.svg' : ''} />
|
<img src={!isRTL ? '../resources/icon/icon-left-to-right.svg' : ''} />
|
||||||
@@ -14,7 +12,9 @@ const ToggleDirectionButton = ({
|
|||||||
<div styleName={!isRTL ? 'active' : undefined} onClick={() => onClick()}>
|
<div styleName={!isRTL ? 'active' : undefined} onClick={() => onClick()}>
|
||||||
<img src={!isRTL ? '' : '../resources/icon/icon-right-to-left.svg'} />
|
<img src={!isRTL ? '' : '../resources/icon/icon-right-to-left.svg'} />
|
||||||
</div>
|
</div>
|
||||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Direction')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>
|
||||||
|
{i18n.__('Toggle Direction')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,35 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './ToggleModeButton.styl'
|
import styles from './ToggleModeButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const ToggleModeButton = ({
|
const ToggleModeButton = ({ onClick, editorType }) => (
|
||||||
onClick, editorType
|
|
||||||
}) => (
|
|
||||||
<div styleName='control-toggleModeButton'>
|
<div styleName='control-toggleModeButton'>
|
||||||
<div styleName={editorType === 'SPLIT' ? 'active' : undefined} onClick={() => onClick('SPLIT')}>
|
<div
|
||||||
<img src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
styleName={editorType === 'SPLIT' ? 'active' : undefined}
|
||||||
|
onClick={() => onClick('SPLIT')}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
editorType === 'EDITOR_PREVIEW'
|
||||||
|
? '../resources/icon/icon-mode-markdown-off-active.svg'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined} onClick={() => onClick('EDITOR_PREVIEW')}>
|
<div
|
||||||
<img src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined}
|
||||||
|
onClick={() => onClick('EDITOR_PREVIEW')}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
editorType === 'EDITOR_PREVIEW'
|
||||||
|
? ''
|
||||||
|
: '../resources/icon/icon-mode-split-on-active.svg'
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>
|
||||||
|
{i18n.__('Toggle Mode')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './TrashButton.styl'
|
import styles from './TrashButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const TrashButton = ({
|
const TrashButton = ({ onClick }) => (
|
||||||
onClick
|
<button styleName='control-trashButton' onClick={e => onClick(e)}>
|
||||||
}) => (
|
|
||||||
<button styleName='control-trashButton'
|
|
||||||
onClick={(e) => onClick(e)}
|
|
||||||
>
|
|
||||||
<img src='../resources/icon/icon-trash.svg' />
|
<img src='../resources/icon/icon-trash.svg' />
|
||||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>
|
||||||
|
{i18n.__('Trash')}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -37,30 +37,44 @@ class Detail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { location, data, match: { params }, config } = this.props
|
const {
|
||||||
const noteKey = location.search !== '' && queryString.parse(location.search).key
|
location,
|
||||||
|
data,
|
||||||
|
match: { params },
|
||||||
|
config
|
||||||
|
} = this.props
|
||||||
|
const noteKey =
|
||||||
|
location.search !== '' && queryString.parse(location.search).key
|
||||||
let note = null
|
let note = null
|
||||||
|
|
||||||
if (location.search !== '') {
|
if (location.search !== '') {
|
||||||
const allNotes = data.noteMap.map(note => note)
|
const allNotes = data.noteMap.map(note => note)
|
||||||
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
|
const trashedNotes = data.trashedSet
|
||||||
|
.toJS()
|
||||||
|
.map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||||
let displayedNotes = allNotes
|
let displayedNotes = allNotes
|
||||||
|
|
||||||
if (location.pathname.match(/\/searched/)) {
|
if (location.pathname.match(/\/searched/)) {
|
||||||
const searchStr = params.searchword
|
const searchStr = params.searchword
|
||||||
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
|
displayedNotes =
|
||||||
|
searchStr === undefined || searchStr === ''
|
||||||
|
? allNotes
|
||||||
: searchFromNotes(allNotes, searchStr)
|
: searchFromNotes(allNotes, searchStr)
|
||||||
} else if (location.pathname.match(/^\/tags/)) {
|
} else if (location.pathname.match(/^\/tags/)) {
|
||||||
const listOfTags = params.tagname.split(' ')
|
const listOfTags = params.tagname.split(' ')
|
||||||
displayedNotes = data.noteMap.map(note => note).filter(note =>
|
displayedNotes = data.noteMap
|
||||||
listOfTags.every(tag => note.tags.includes(tag))
|
.map(note => note)
|
||||||
)
|
.filter(note => listOfTags.every(tag => note.tags.includes(tag)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname.match(/^\/trashed/)) {
|
if (location.pathname.match(/^\/trashed/)) {
|
||||||
displayedNotes = trashedNotes
|
displayedNotes = trashedNotes
|
||||||
} else {
|
} else {
|
||||||
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
|
displayedNotes = _.differenceWith(
|
||||||
|
displayedNotes,
|
||||||
|
trashedNotes,
|
||||||
|
(note, trashed) => note.key === trashed.key
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteKeys = displayedNotes.map(note => note.key)
|
const noteKeys = displayedNotes.map(note => note.key)
|
||||||
@@ -71,12 +85,12 @@ class Detail extends React.Component {
|
|||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
return (
|
return (
|
||||||
<div styleName='root'
|
<div styleName='root' style={this.props.style} tabIndex='0'>
|
||||||
style={this.props.style}
|
|
||||||
tabIndex='0'
|
|
||||||
>
|
|
||||||
<div styleName='empty'>
|
<div styleName='empty'>
|
||||||
<div styleName='empty-message'>{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />{i18n.__('to create a new note')}</div>
|
<div styleName='empty-message'>
|
||||||
|
{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />
|
||||||
|
{i18n.__('to create a new note')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||||
|
|||||||
@@ -93,18 +93,21 @@ class Main extends React.Component {
|
|||||||
type: 'SNIPPET_NOTE',
|
type: 'SNIPPET_NOTE',
|
||||||
folder: data.storage.folders[0].key,
|
folder: data.storage.folders[0].key,
|
||||||
title: 'Snippet note example',
|
title: 'Snippet note example',
|
||||||
description: 'Snippet note example\nYou can store a series of snippets as a single note, like Gist.',
|
description:
|
||||||
|
'Snippet note example\nYou can store a series of snippets as a single note, like Gist.',
|
||||||
snippets: [
|
snippets: [
|
||||||
{
|
{
|
||||||
name: 'example.html',
|
name: 'example.html',
|
||||||
mode: 'html',
|
mode: 'html',
|
||||||
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
|
content:
|
||||||
|
"<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
|
||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'example.js',
|
name: 'example.js',
|
||||||
mode: 'javascript',
|
mode: 'javascript',
|
||||||
content: "var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)",
|
content:
|
||||||
|
"var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)",
|
||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -120,7 +123,8 @@ class Main extends React.Component {
|
|||||||
type: 'MARKDOWN_NOTE',
|
type: 'MARKDOWN_NOTE',
|
||||||
folder: data.storage.folders[0].key,
|
folder: data.storage.folders[0].key,
|
||||||
title: 'Welcome to Boostnote!',
|
title: 'Welcome to Boostnote!',
|
||||||
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
content:
|
||||||
|
'# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
||||||
})
|
})
|
||||||
.then(note => {
|
.then(note => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
@@ -173,12 +177,18 @@ class Main extends React.Component {
|
|||||||
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
||||||
|
|
||||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||||
eventEmitter.on('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
|
eventEmitter.on(
|
||||||
|
'menubar:togglemenubar',
|
||||||
|
this.toggleMenuBarVisible.bind(this)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
|
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
|
||||||
eventEmitter.off('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
|
eventEmitter.off(
|
||||||
|
'menubar:togglemenubar',
|
||||||
|
this.toggleMenuBarVisible.bind(this)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMenuBarVisible() {
|
toggleMenuBarVisible() {
|
||||||
@@ -312,10 +322,16 @@ class Main extends React.Component {
|
|||||||
onMouseUp={e => this.handleMouseUp(e)}
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
>
|
>
|
||||||
<SideNav
|
<SideNav
|
||||||
{..._.pick(this.props, ['dispatch', 'data', 'config', 'match', 'location'])}
|
{..._.pick(this.props, [
|
||||||
|
'dispatch',
|
||||||
|
'data',
|
||||||
|
'config',
|
||||||
|
'match',
|
||||||
|
'location'
|
||||||
|
])}
|
||||||
width={this.state.navWidth}
|
width={this.state.navWidth}
|
||||||
/>
|
/>
|
||||||
{!config.isSideNavFolded &&
|
{!config.isSideNavFolded && (
|
||||||
<div
|
<div
|
||||||
styleName={
|
styleName={
|
||||||
this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
|
this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
|
||||||
@@ -325,7 +341,8 @@ class Main extends React.Component {
|
|||||||
draggable='false'
|
draggable='false'
|
||||||
>
|
>
|
||||||
<div styleName='slider-hitbox' />
|
<div styleName='slider-hitbox' />
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||||
id='main-body'
|
id='main-body'
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ class NewNoteButton extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {}
|
||||||
}
|
|
||||||
|
|
||||||
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
|
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
|
||||||
}
|
}
|
||||||
@@ -33,12 +32,31 @@ class NewNoteButton extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNewNoteButtonClick(e) {
|
handleNewNoteButtonClick(e) {
|
||||||
const { location, dispatch, match: { params }, config } = this.props
|
const {
|
||||||
|
location,
|
||||||
|
dispatch,
|
||||||
|
match: { params },
|
||||||
|
config
|
||||||
|
} = this.props
|
||||||
const { storage, folder } = this.resolveTargetFolder()
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||||
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
createMarkdownNote(
|
||||||
|
storage.key,
|
||||||
|
folder.key,
|
||||||
|
dispatch,
|
||||||
|
location,
|
||||||
|
params,
|
||||||
|
config
|
||||||
|
)
|
||||||
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||||
createSnippetNote(storage.key, folder.key, dispatch, location, params, config)
|
createSnippetNote(
|
||||||
|
storage.key,
|
||||||
|
folder.key,
|
||||||
|
dispatch,
|
||||||
|
location,
|
||||||
|
params,
|
||||||
|
config
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
modal.open(NewNoteModal, {
|
modal.open(NewNoteModal, {
|
||||||
storage: storage.key,
|
storage: storage.key,
|
||||||
@@ -52,7 +70,10 @@ class NewNoteButton extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolveTargetFolder() {
|
resolveTargetFolder() {
|
||||||
const { data, match: { params } } = this.props
|
const {
|
||||||
|
data,
|
||||||
|
match: { params }
|
||||||
|
} = this.props
|
||||||
let storage = data.storageMap.get(params.storageKey)
|
let storage = data.storageMap.get(params.storageKey)
|
||||||
// Find first storage
|
// Find first storage
|
||||||
if (storage == null) {
|
if (storage == null) {
|
||||||
@@ -62,9 +83,12 @@ class NewNoteButton extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storage == null) this.showMessageBox(i18n.__('No storage to create a note'))
|
if (storage == null)
|
||||||
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
|
this.showMessageBox(i18n.__('No storage to create a note'))
|
||||||
if (folder == null) this.showMessageBox(i18n.__('No folder to create a note'))
|
const folder =
|
||||||
|
_.find(storage.folders, { key: params.folderKey }) || storage.folders[0]
|
||||||
|
if (folder == null)
|
||||||
|
this.showMessageBox(i18n.__('No folder to create a note'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
storage,
|
storage,
|
||||||
@@ -83,13 +107,16 @@ class NewNoteButton extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { config, style } = this.props
|
const { config, style } = this.props
|
||||||
return (
|
return (
|
||||||
<div className='NewNoteButton'
|
<div
|
||||||
|
className='NewNoteButton'
|
||||||
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
|
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<div styleName='control'>
|
<div styleName='control'>
|
||||||
<button styleName='control-newNoteButton'
|
<button
|
||||||
onClick={this.handleNewNoteButtonClick}>
|
styleName='control-newNoteButton'
|
||||||
|
onClick={this.handleNewNoteButtonClick}
|
||||||
|
>
|
||||||
<img src='../resources/icon/icon-newnote.svg' />
|
<img src='../resources/icon/icon-newnote.svg' />
|
||||||
<span styleName='control-newNoteButton-tooltip'>
|
<span styleName='control-newNoteButton-tooltip'>
|
||||||
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const { remote } = require('electron')
|
|||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
const WP_POST_PATH = '/wp/v2/posts'
|
const WP_POST_PATH = '/wp/v2/posts'
|
||||||
|
|
||||||
const regexMatchStartingTitleNumber = new RegExp('^([0-9]*\.?[0-9]+).*$')
|
const regexMatchStartingTitleNumber = new RegExp('^([0-9]*.?[0-9]+).*$')
|
||||||
|
|
||||||
function sortByCreatedAt(a, b) {
|
function sortByCreatedAt(a, b) {
|
||||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||||
@@ -58,11 +58,11 @@ function sortByUpdatedAt (a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findNoteByKey(notes, noteKey) {
|
function findNoteByKey(notes, noteKey) {
|
||||||
return notes.find((note) => note.key === noteKey)
|
return notes.find(note => note.key === noteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
function findNotesByKeys(notes, noteKeys) {
|
function findNotesByKeys(notes, noteKeys) {
|
||||||
return notes.filter((note) => noteKeys.includes(getNoteKey(note)))
|
return notes.filter(note => noteKeys.includes(getNoteKey(note)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteKey(note) {
|
function getNoteKey(note) {
|
||||||
@@ -152,11 +152,16 @@ class NoteList extends React.Component {
|
|||||||
const visibleNoteKeys = this.notes && this.notes.map(note => note.key)
|
const visibleNoteKeys = this.notes && this.notes.map(note => note.key)
|
||||||
const note = this.notes && this.notes[0]
|
const note = this.notes && this.notes[0]
|
||||||
const key = location.search && queryString.parse(location.search).key
|
const key = location.search && queryString.parse(location.search).key
|
||||||
const prevKey = prevProps.location.search && queryString.parse(prevProps.location.search).key
|
const prevKey =
|
||||||
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
prevProps.location.search &&
|
||||||
|
queryString.parse(prevProps.location.search).key
|
||||||
|
const noteKey = visibleNoteKeys.includes(prevKey)
|
||||||
|
? prevKey
|
||||||
|
: note && note.key
|
||||||
|
|
||||||
if (note && location.search === '') {
|
if (note && location.search === '') {
|
||||||
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
if (!location.pathname.match(/\/searched/))
|
||||||
|
this.contextNotes = this.getContextNotes()
|
||||||
|
|
||||||
// A visible note is an active note
|
// A visible note is an active note
|
||||||
if (!selectedNoteKeys.includes(noteKey)) {
|
if (!selectedNoteKeys.includes(noteKey)) {
|
||||||
@@ -165,12 +170,15 @@ class NoteList extends React.Component {
|
|||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(replace({ // was passed with context - we can use connected router here
|
dispatch(
|
||||||
|
replace({
|
||||||
|
// was passed with context - we can use connected router here
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: queryString.stringify({
|
search: queryString.stringify({
|
||||||
key: noteKey
|
key: noteKey
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,9 +191,15 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
if (item == null) return false
|
if (item == null) return false
|
||||||
|
|
||||||
const overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
const overflowBelow =
|
||||||
|
item.offsetTop +
|
||||||
|
item.clientHeight -
|
||||||
|
list.clientHeight -
|
||||||
|
list.scrollTop >
|
||||||
|
0
|
||||||
if (overflowBelow) {
|
if (overflowBelow) {
|
||||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
list.scrollTop =
|
||||||
|
item.offsetTop + item.clientHeight - list.clientHeight
|
||||||
}
|
}
|
||||||
const overflowAbove = list.scrollTop > item.offsetTop
|
const overflowAbove = list.scrollTop > item.offsetTop
|
||||||
if (overflowAbove) {
|
if (overflowAbove) {
|
||||||
@@ -202,12 +216,14 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys
|
selectedNoteKeys
|
||||||
})
|
})
|
||||||
|
|
||||||
dispatch(push({
|
dispatch(
|
||||||
|
push({
|
||||||
pathname,
|
pathname,
|
||||||
search: queryString.stringify({
|
search: queryString.stringify({
|
||||||
key: noteKey
|
key: noteKey
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteKeyFromTargetIndex(targetIndex) {
|
getNoteKeyFromTargetIndex(targetIndex) {
|
||||||
@@ -231,7 +247,9 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
targetIndex--
|
targetIndex--
|
||||||
|
|
||||||
if (!shiftKeyDown) { selectedNoteKeys = [] }
|
if (!shiftKeyDown) {
|
||||||
|
selectedNoteKeys = []
|
||||||
|
}
|
||||||
const priorNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
|
const priorNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
|
||||||
if (selectedNoteKeys.includes(priorNoteKey)) {
|
if (selectedNoteKeys.includes(priorNoteKey)) {
|
||||||
selectedNoteKeys.pop()
|
selectedNoteKeys.pop()
|
||||||
@@ -262,10 +280,13 @@ class NoteList extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
targetIndex++
|
targetIndex++
|
||||||
if (targetIndex < 0) targetIndex = 0
|
if (targetIndex < 0) targetIndex = 0
|
||||||
else if (targetIndex > this.notes.length - 1) targetIndex = this.notes.length - 1
|
else if (targetIndex > this.notes.length - 1)
|
||||||
|
targetIndex = this.notes.length - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shiftKeyDown) { selectedNoteKeys = [] }
|
if (!shiftKeyDown) {
|
||||||
|
selectedNoteKeys = []
|
||||||
|
}
|
||||||
const nextNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
|
const nextNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
|
||||||
if (selectedNoteKeys.includes(nextNoteKey)) {
|
if (selectedNoteKeys.includes(nextNoteKey)) {
|
||||||
selectedNoteKeys.pop()
|
selectedNoteKeys.pop()
|
||||||
@@ -289,9 +310,12 @@ class NoteList extends React.Component {
|
|||||||
const selectedNoteKeys = [noteHash]
|
const selectedNoteKeys = [noteHash]
|
||||||
|
|
||||||
let locationToSelect = '/home'
|
let locationToSelect = '/home'
|
||||||
const noteByHash = data.noteMap.map((note) => note).find(note => note.key === noteHash)
|
const noteByHash = data.noteMap
|
||||||
|
.map(note => note)
|
||||||
|
.find(note => note.key === noteHash)
|
||||||
if (noteByHash !== undefined) {
|
if (noteByHash !== undefined) {
|
||||||
locationToSelect = '/storages/' + noteByHash.storage + '/folders/' + noteByHash.folder
|
locationToSelect =
|
||||||
|
'/storages/' + noteByHash.storage + '/folders/' + noteByHash.folder
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focusNote(selectedNoteKeys, noteHash, locationToSelect)
|
this.focusNote(selectedNoteKeys, noteHash, locationToSelect)
|
||||||
@@ -357,22 +381,31 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getNotes() {
|
getNotes() {
|
||||||
const { data, match: { params }, location } = this.props
|
const {
|
||||||
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
|
data,
|
||||||
const allNotes = data.noteMap.map((note) => note)
|
match: { params },
|
||||||
|
location
|
||||||
|
} = this.props
|
||||||
|
if (
|
||||||
|
location.pathname.match(/\/home/) ||
|
||||||
|
location.pathname.match(/alltags/)
|
||||||
|
) {
|
||||||
|
const allNotes = data.noteMap.map(note => note)
|
||||||
this.contextNotes = allNotes
|
this.contextNotes = allNotes
|
||||||
return allNotes
|
return allNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname.match(/\/starred/)) {
|
if (location.pathname.match(/\/starred/)) {
|
||||||
const starredNotes = data.starredSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
const starredNotes = data.starredSet
|
||||||
|
.toJS()
|
||||||
|
.map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||||
this.contextNotes = starredNotes
|
this.contextNotes = starredNotes
|
||||||
return starredNotes
|
return starredNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname.match(/\/searched/)) {
|
if (location.pathname.match(/\/searched/)) {
|
||||||
const searchInputText = params.searchword
|
const searchInputText = params.searchword
|
||||||
const allNotes = data.noteMap.map((note) => note)
|
const allNotes = data.noteMap.map(note => note)
|
||||||
this.contextNotes = allNotes
|
this.contextNotes = allNotes
|
||||||
if (searchInputText === undefined || searchInputText === '') {
|
if (searchInputText === undefined || searchInputText === '') {
|
||||||
return this.sortByPin(this.contextNotes)
|
return this.sortByPin(this.contextNotes)
|
||||||
@@ -381,16 +414,20 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname.match(/\/trashed/)) {
|
if (location.pathname.match(/\/trashed/)) {
|
||||||
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
const trashedNotes = data.trashedSet
|
||||||
|
.toJS()
|
||||||
|
.map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||||
this.contextNotes = trashedNotes
|
this.contextNotes = trashedNotes
|
||||||
return trashedNotes
|
return trashedNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname.match(/\/tags/)) {
|
if (location.pathname.match(/\/tags/)) {
|
||||||
const listOfTags = params.tagname.split(' ')
|
const listOfTags = params.tagname.split(' ')
|
||||||
return data.noteMap.map(note => {
|
return data.noteMap
|
||||||
|
.map(note => {
|
||||||
return note
|
return note
|
||||||
}).filter(note => listOfTags.every(tag => note.tags.includes(tag)))
|
})
|
||||||
|
.filter(note => listOfTags.every(tag => note.tags.includes(tag)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getContextNotes()
|
return this.getContextNotes()
|
||||||
@@ -398,7 +435,10 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
// get notes in the current folder
|
// get notes in the current folder
|
||||||
getContextNotes() {
|
getContextNotes() {
|
||||||
const { data, match: { params } } = this.props
|
const {
|
||||||
|
data,
|
||||||
|
match: { params }
|
||||||
|
} = this.props
|
||||||
const storageKey = params.storageKey
|
const storageKey = params.storageKey
|
||||||
const folderKey = params.folderKey
|
const folderKey = params.folderKey
|
||||||
const storage = data.storageMap.get(storageKey)
|
const storage = data.storageMap.get(storageKey)
|
||||||
@@ -407,18 +447,19 @@ class NoteList extends React.Component {
|
|||||||
const folder = _.find(storage.folders, { key: folderKey })
|
const folder = _.find(storage.folders, { key: folderKey })
|
||||||
if (folder === undefined) {
|
if (folder === undefined) {
|
||||||
const storageNoteSet = data.storageNoteMap.get(storage.key) || []
|
const storageNoteSet = data.storageNoteMap.get(storage.key) || []
|
||||||
return storageNoteSet.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
return storageNoteSet.map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderNoteKeyList = data.folderNoteMap.get(`${storage.key}-${folder.key}`) || []
|
const folderNoteKeyList =
|
||||||
return folderNoteKeyList.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
data.folderNoteMap.get(`${storage.key}-${folder.key}`) || []
|
||||||
|
return folderNoteKeyList.map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
sortByPin(unorderedNotes) {
|
sortByPin(unorderedNotes) {
|
||||||
const pinnedNotes = []
|
const pinnedNotes = []
|
||||||
const unpinnedNotes = []
|
const unpinnedNotes = []
|
||||||
|
|
||||||
unorderedNotes.forEach((note) => {
|
unorderedNotes.forEach(note => {
|
||||||
if (note.isPinned) {
|
if (note.isPinned) {
|
||||||
pinnedNotes.push(note)
|
pinnedNotes.push(note)
|
||||||
} else {
|
} else {
|
||||||
@@ -430,7 +471,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getNoteIndexByKey(noteKey) {
|
getNoteIndexByKey(noteKey) {
|
||||||
return this.notes.findIndex((note) => {
|
return this.notes.findIndex(note => {
|
||||||
if (!note) return -1
|
if (!note) return -1
|
||||||
|
|
||||||
return note.key === noteKey
|
return note.key === noteKey
|
||||||
@@ -444,7 +485,9 @@ class NoteList extends React.Component {
|
|||||||
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||||
|
|
||||||
if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||||
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
const newSelectedNoteKeys = selectedNoteKeys.filter(
|
||||||
|
noteKey => noteKey !== uniqueKey
|
||||||
|
)
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedNoteKeys: newSelectedNoteKeys
|
selectedNoteKeys: newSelectedNoteKeys
|
||||||
})
|
})
|
||||||
@@ -464,15 +507,21 @@ class NoteList extends React.Component {
|
|||||||
let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0])
|
let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0])
|
||||||
// Shift selection can either start from first note in the exisiting selectedNoteKeys
|
// Shift selection can either start from first note in the exisiting selectedNoteKeys
|
||||||
// or previous first shift note index
|
// or previous first shift note index
|
||||||
firstShiftNoteIndex = firstShiftNoteIndex > prevShiftNoteIndex
|
firstShiftNoteIndex =
|
||||||
? firstShiftNoteIndex : prevShiftNoteIndex
|
firstShiftNoteIndex > prevShiftNoteIndex
|
||||||
|
? firstShiftNoteIndex
|
||||||
|
: prevShiftNoteIndex
|
||||||
|
|
||||||
const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey)
|
const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey)
|
||||||
|
|
||||||
const startIndex = firstShiftNoteIndex < lastShiftNoteIndex
|
const startIndex =
|
||||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
firstShiftNoteIndex < lastShiftNoteIndex
|
||||||
const endIndex = firstShiftNoteIndex > lastShiftNoteIndex
|
? firstShiftNoteIndex
|
||||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
: lastShiftNoteIndex
|
||||||
|
const endIndex =
|
||||||
|
firstShiftNoteIndex > lastShiftNoteIndex
|
||||||
|
? firstShiftNoteIndex
|
||||||
|
: lastShiftNoteIndex
|
||||||
|
|
||||||
selectedNoteKeys = []
|
selectedNoteKeys = []
|
||||||
for (let i = startIndex; i <= endIndex; i++) {
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
@@ -489,16 +538,23 @@ class NoteList extends React.Component {
|
|||||||
prevShiftNoteIndex
|
prevShiftNoteIndex
|
||||||
})
|
})
|
||||||
|
|
||||||
dispatch(push({
|
dispatch(
|
||||||
|
push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: queryString.stringify({
|
search: queryString.stringify({
|
||||||
key: uniqueKey
|
key: uniqueKey
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortByChange(e) {
|
handleSortByChange(e) {
|
||||||
const { dispatch, match: { params: { folderKey } } } = this.props
|
const {
|
||||||
|
dispatch,
|
||||||
|
match: {
|
||||||
|
params: { folderKey }
|
||||||
|
}
|
||||||
|
} = this.props
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
[folderKey]: { sortBy: e.target.value }
|
[folderKey]: { sortBy: e.target.value }
|
||||||
@@ -526,20 +582,23 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
alertIfSnippet(msg) {
|
alertIfSnippet(msg) {
|
||||||
const warningMessage = (msg) => ({
|
const warningMessage = msg =>
|
||||||
|
({
|
||||||
'export-txt': 'Text export',
|
'export-txt': 'Text export',
|
||||||
'export-md': 'Markdown export',
|
'export-md': 'Markdown export',
|
||||||
'export-html': 'HTML export',
|
'export-html': 'HTML export',
|
||||||
'export-pdf': 'PDF export',
|
'export-pdf': 'PDF export',
|
||||||
'print': 'Print'
|
print: 'Print'
|
||||||
})[msg]
|
}[msg])
|
||||||
|
|
||||||
const targetIndex = this.getTargetIndex()
|
const targetIndex = this.getTargetIndex()
|
||||||
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Sorry!'),
|
message: i18n.__('Sorry!'),
|
||||||
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
detail: i18n.__(
|
||||||
|
warningMessage(msg) + ' is available only in markdown notes.'
|
||||||
|
),
|
||||||
buttons: [i18n.__('OK')]
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -554,7 +613,7 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys.push(noteKey)
|
selectedNoteKeys.push(noteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
const notes = this.notes.map(note => Object.assign({}, note))
|
||||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
const noteData = JSON.stringify(selectedNotes)
|
const noteData = JSON.stringify(selectedNotes)
|
||||||
e.dataTransfer.setData('note', noteData)
|
e.dataTransfer.setData('note', noteData)
|
||||||
@@ -571,7 +630,9 @@ class NoteList extends React.Component {
|
|||||||
this.handleNoteClick(e, uniqueKey)
|
this.handleNoteClick(e, uniqueKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinLabel = note.isPinned ? i18n.__('Remove pin') : i18n.__('Pin to Top')
|
const pinLabel = note.isPinned
|
||||||
|
? i18n.__('Remove pin')
|
||||||
|
: i18n.__('Pin to Top')
|
||||||
const deleteLabel = i18n.__('Delete Note')
|
const deleteLabel = i18n.__('Delete Note')
|
||||||
const cloneNote = i18n.__('Clone Note')
|
const cloneNote = i18n.__('Clone Note')
|
||||||
const restoreNote = i18n.__('Restore Note')
|
const restoreNote = i18n.__('Restore Note')
|
||||||
@@ -583,13 +644,16 @@ class NoteList extends React.Component {
|
|||||||
const templates = []
|
const templates = []
|
||||||
|
|
||||||
if (location.pathname.match(/\/trash/)) {
|
if (location.pathname.match(/\/trash/)) {
|
||||||
templates.push({
|
templates.push(
|
||||||
|
{
|
||||||
label: restoreNote,
|
label: restoreNote,
|
||||||
click: this.restoreNote
|
click: this.restoreNote
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
label: deleteLabel,
|
label: deleteLabel,
|
||||||
click: this.deleteNote
|
click: this.deleteNote
|
||||||
})
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
if (!location.pathname.match(/\/starred/)) {
|
if (!location.pathname.match(/\/starred/)) {
|
||||||
templates.push({
|
templates.push({
|
||||||
@@ -597,25 +661,32 @@ class NoteList extends React.Component {
|
|||||||
click: this.pinToTop
|
click: this.pinToTop
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
templates.push({
|
templates.push(
|
||||||
|
{
|
||||||
label: deleteLabel,
|
label: deleteLabel,
|
||||||
click: this.deleteNote
|
click: this.deleteNote
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
label: cloneNote,
|
label: cloneNote,
|
||||||
click: this.cloneNote.bind(this)
|
click: this.cloneNote.bind(this)
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
label: copyNoteLink,
|
label: copyNoteLink,
|
||||||
click: this.copyNoteLink.bind(this, note)
|
click: this.copyNoteLink.bind(this, note)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
if (note.type === 'MARKDOWN_NOTE') {
|
if (note.type === 'MARKDOWN_NOTE') {
|
||||||
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||||
templates.push({
|
templates.push(
|
||||||
|
{
|
||||||
label: updateLabel,
|
label: updateLabel,
|
||||||
click: this.publishMarkdown.bind(this)
|
click: this.publishMarkdown.bind(this)
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
label: openBlogLabel,
|
label: openBlogLabel,
|
||||||
click: () => this.openBlog.bind(this)(note)
|
click: () => this.openBlog.bind(this)(note)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
templates.push({
|
templates.push({
|
||||||
label: publishLabel,
|
label: publishLabel,
|
||||||
@@ -630,23 +701,23 @@ class NoteList extends React.Component {
|
|||||||
updateSelectedNotes(updateFunc, cleanSelection = true) {
|
updateSelectedNotes(updateFunc, cleanSelection = true) {
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
const notes = this.notes.map(note => Object.assign({}, note))
|
||||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
|
||||||
if (!_.isFunction(updateFunc)) {
|
if (!_.isFunction(updateFunc)) {
|
||||||
console.warn('Update function is not defined. No update will happen')
|
console.warn('Update function is not defined. No update will happen')
|
||||||
updateFunc = (note) => { return note }
|
updateFunc = note => {
|
||||||
|
return note
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
selectedNotes.map((note) => {
|
selectedNotes.map(note => {
|
||||||
note = updateFunc(note)
|
note = updateFunc(note)
|
||||||
return dataApi
|
return dataApi.updateNote(note.storage, note.key, note)
|
||||||
.updateNote(note.storage, note.key, note)
|
|
||||||
})
|
})
|
||||||
)
|
).then(updatedNotes => {
|
||||||
.then((updatedNotes) => {
|
updatedNotes.forEach(note => {
|
||||||
updatedNotes.forEach((note) => {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note
|
note
|
||||||
@@ -660,14 +731,14 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pinToTop() {
|
pinToTop() {
|
||||||
this.updateSelectedNotes((note) => {
|
this.updateSelectedNotes(note => {
|
||||||
note.isPinned = !note.isPinned
|
note.isPinned = !note.isPinned
|
||||||
return note
|
return note
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreNote() {
|
restoreNote() {
|
||||||
this.updateSelectedNotes((note) => {
|
this.updateSelectedNotes(note => {
|
||||||
note.isTrashed = false
|
note.isTrashed = false
|
||||||
return note
|
return note
|
||||||
})
|
})
|
||||||
@@ -676,7 +747,7 @@ class NoteList extends React.Component {
|
|||||||
deleteNote() {
|
deleteNote() {
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
const notes = this.notes.map(note => Object.assign({}, note))
|
||||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
const firstNote = selectedNotes[0]
|
const firstNote = selectedNotes[0]
|
||||||
const { confirmDeletion } = this.props.config.ui
|
const { confirmDeletion } = this.props.config.ui
|
||||||
@@ -685,14 +756,13 @@ class NoteList extends React.Component {
|
|||||||
if (!confirmDeleteNote(confirmDeletion, true)) return
|
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
selectedNotes.map((note) => {
|
selectedNotes.map(note => {
|
||||||
return dataApi
|
return dataApi.deleteNote(note.storage, note.key)
|
||||||
.deleteNote(note.storage, note.key)
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
const dispatchHandler = () => {
|
const dispatchHandler = () => {
|
||||||
data.forEach((item) => {
|
data.forEach(item => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'DELETE_NOTE',
|
type: 'DELETE_NOTE',
|
||||||
storageKey: item.storageKey,
|
storageKey: item.storageKey,
|
||||||
@@ -703,22 +773,21 @@ class NoteList extends React.Component {
|
|||||||
ee.once('list:next', dispatchHandler)
|
ee.once('list:next', dispatchHandler)
|
||||||
})
|
})
|
||||||
.then(() => ee.emit('list:next'))
|
.then(() => ee.emit('list:next'))
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
console.error('Cannot Delete note: ' + err)
|
console.error('Cannot Delete note: ' + err)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (!confirmDeleteNote(confirmDeletion, false)) return
|
if (!confirmDeleteNote(confirmDeletion, false)) return
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
selectedNotes.map((note) => {
|
selectedNotes.map(note => {
|
||||||
note.isTrashed = true
|
note.isTrashed = true
|
||||||
|
|
||||||
return dataApi
|
return dataApi.updateNote(note.storage, note.key, note)
|
||||||
.updateNote(note.storage, note.key, note)
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.then((newNotes) => {
|
.then(newNotes => {
|
||||||
newNotes.forEach((newNote) => {
|
newNotes.forEach(newNote => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: newNote
|
note: newNote
|
||||||
@@ -727,7 +796,7 @@ class NoteList extends React.Component {
|
|||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||||
})
|
})
|
||||||
.then(() => ee.emit('list:next'))
|
.then(() => ee.emit('list:next'))
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
console.error('Notes could not go to trash: ' + err)
|
console.error('Notes could not go to trash: ' + err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -738,10 +807,11 @@ class NoteList extends React.Component {
|
|||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const { dispatch, location } = this.props
|
const { dispatch, location } = this.props
|
||||||
const { storage, folder } = this.resolveTargetFolder()
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
const notes = this.notes.map(note => Object.assign({}, note))
|
||||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
const firstNote = selectedNotes[0]
|
const firstNote = selectedNotes[0]
|
||||||
const eventName = firstNote.type === 'MARKDOWN_NOTE' ? 'ADD_MARKDOWN' : 'ADD_SNIPPET'
|
const eventName =
|
||||||
|
firstNote.type === 'MARKDOWN_NOTE' ? 'ADD_MARKDOWN' : 'ADD_SNIPPET'
|
||||||
|
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent(eventName)
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent(eventName)
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||||
@@ -757,11 +827,11 @@ class NoteList extends React.Component {
|
|||||||
tags: firstNote.tags,
|
tags: firstNote.tags,
|
||||||
isStarred: firstNote.isStarred
|
isStarred: firstNote.isStarred
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then(note => {
|
||||||
attachmentManagement.cloneAttachments(firstNote, note)
|
attachmentManagement.cloneAttachments(firstNote, note)
|
||||||
return note
|
return note
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then(note => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
@@ -771,10 +841,12 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys: [note.key]
|
selectedNoteKeys: [note.key]
|
||||||
})
|
})
|
||||||
|
|
||||||
dispatch(push({
|
dispatch(
|
||||||
|
push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: queryString.stringify({ key: note.key })
|
search: queryString.stringify({ key: note.key })
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,19 +857,19 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
navigate(sender, pathname) {
|
navigate(sender, pathname) {
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
dispatch(push({
|
dispatch(
|
||||||
|
push({
|
||||||
pathname,
|
pathname,
|
||||||
search: queryString.stringify({
|
search: queryString.stringify({
|
||||||
// key: noteKey
|
// key: noteKey
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
save(note) {
|
save(note) {
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
dataApi
|
dataApi.updateNote(note.storage, note.key, note).then(note => {
|
||||||
.updateNote(note.storage, note.key, note)
|
|
||||||
.then((note) => {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
@@ -816,7 +888,7 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
publishMarkdownNow() {
|
publishMarkdownNow() {
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
const notes = this.notes.map(note => Object.assign({}, note))
|
||||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
const firstNote = selectedNotes[0]
|
const firstNote = selectedNotes[0]
|
||||||
const config = ConfigManager.get()
|
const config = ConfigManager.get()
|
||||||
@@ -827,7 +899,10 @@ class NoteList extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
authToken = `Bearer ${token}`
|
authToken = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
|
const contentToRender = firstNote.content.replace(
|
||||||
|
`# ${firstNote.title}`,
|
||||||
|
''
|
||||||
|
)
|
||||||
const markdown = new Markdown()
|
const markdown = new Markdown()
|
||||||
const data = {
|
const data = {
|
||||||
title: firstNote.title,
|
title: firstNote.title,
|
||||||
@@ -849,10 +924,11 @@ class NoteList extends React.Component {
|
|||||||
method: method,
|
method: method,
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': authToken,
|
Authorization: authToken,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}).then(res => res.json())
|
})
|
||||||
|
.then(res => res.json())
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (_.isNil(response.link) || _.isNil(response.id)) {
|
if (_.isNil(response.link) || _.isNil(response.id)) {
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
@@ -864,7 +940,7 @@ class NoteList extends React.Component {
|
|||||||
this.save(firstNote)
|
this.save(firstNote)
|
||||||
this.confirmPublish(firstNote)
|
this.confirmPublish(firstNote)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(error => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
this.confirmPublishError()
|
this.confirmPublishError()
|
||||||
})
|
})
|
||||||
@@ -902,13 +978,11 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
importFromFile() {
|
importFromFile() {
|
||||||
const options = {
|
const options = {
|
||||||
filters: [
|
filters: [{ name: 'Documents', extensions: ['md', 'txt'] }],
|
||||||
{ name: 'Documents', extensions: ['md', 'txt'] }
|
|
||||||
],
|
|
||||||
properties: ['openFile', 'multiSelections']
|
properties: ['openFile', 'multiSelections']
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, (filepaths) => {
|
dialog.showOpenDialog(remote.getCurrentWindow(), options, filepaths => {
|
||||||
this.addNotesFromFiles(filepaths)
|
this.addNotesFromFiles(filepaths)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -916,7 +990,9 @@ class NoteList extends React.Component {
|
|||||||
handleDrop(e) {
|
handleDrop(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
const filepaths = Array.from(e.dataTransfer.files).map(file => { return file.path })
|
const filepaths = Array.from(e.dataTransfer.files).map(file => {
|
||||||
|
return file.path
|
||||||
|
})
|
||||||
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
|
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,7 +1002,7 @@ class NoteList extends React.Component {
|
|||||||
const { storage, folder } = this.resolveTargetFolder()
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
|
|
||||||
if (filepaths === undefined) return
|
if (filepaths === undefined) return
|
||||||
filepaths.forEach((filepath) => {
|
filepaths.forEach(filepath => {
|
||||||
fs.readFile(filepath, (err, data) => {
|
fs.readFile(filepath, (err, data) => {
|
||||||
if (err) throw Error('File reading error: ', err)
|
if (err) throw Error('File reading error: ', err)
|
||||||
|
|
||||||
@@ -942,10 +1018,10 @@ class NoteList extends React.Component {
|
|||||||
createdAt: birthtime,
|
createdAt: birthtime,
|
||||||
updatedAt: mtime
|
updatedAt: mtime
|
||||||
}
|
}
|
||||||
dataApi.createNote(storage.key, newNote)
|
dataApi.createNote(storage.key, newNote).then(note => {
|
||||||
.then((note) => {
|
attachmentManagement
|
||||||
attachmentManagement.importAttachments(note.content, filepath, storage.key, note.key)
|
.importAttachments(note.content, filepath, storage.key, note.key)
|
||||||
.then((newcontent) => {
|
.then(newcontent => {
|
||||||
note.content = newcontent
|
note.content = newcontent
|
||||||
|
|
||||||
dataApi.updateNote(storage.key, note.key, note)
|
dataApi.updateNote(storage.key, note.key, note)
|
||||||
@@ -954,10 +1030,12 @@ class NoteList extends React.Component {
|
|||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
dispatch(push({
|
dispatch(
|
||||||
|
push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: queryString.stringify({ key: getNoteKey(note) })
|
search: queryString.stringify({ key: getNoteKey(note) })
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -968,14 +1046,17 @@ class NoteList extends React.Component {
|
|||||||
getTargetIndex() {
|
getTargetIndex() {
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
const key = queryString.parse(location.search).key
|
const key = queryString.parse(location.search).key
|
||||||
const targetIndex = _.findIndex(this.notes, (note) => {
|
const targetIndex = _.findIndex(this.notes, note => {
|
||||||
return getNoteKey(note) === key
|
return getNoteKey(note) === key
|
||||||
})
|
})
|
||||||
return targetIndex
|
return targetIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveTargetFolder() {
|
resolveTargetFolder() {
|
||||||
const { data, match: { params } } = this.props
|
const {
|
||||||
|
data,
|
||||||
|
match: { params }
|
||||||
|
} = this.props
|
||||||
let storage = data.storageMap.get(params.storageKey)
|
let storage = data.storageMap.get(params.storageKey)
|
||||||
|
|
||||||
// Find first storage
|
// Find first storage
|
||||||
@@ -987,7 +1068,8 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (storage == null) this.showMessageBox('No storage for importing note(s)')
|
if (storage == null) this.showMessageBox('No storage for importing note(s)')
|
||||||
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
|
const folder =
|
||||||
|
_.find(storage.folders, { key: params.folderKey }) || storage.folders[0]
|
||||||
if (folder == null) this.showMessageBox('No folder for importing note(s)')
|
if (folder == null) this.showMessageBox('No folder for importing note(s)')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1004,12 +1086,17 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteStorage (note) { // note.storage = storage key
|
getNoteStorage(note) {
|
||||||
|
// note.storage = storage key
|
||||||
return this.props.data.storageMap.toJS()[note.storage]
|
return this.props.data.storageMap.toJS()[note.storage]
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteFolder (note) { // note.folder = folder key
|
getNoteFolder(note) {
|
||||||
return _.find(this.getNoteStorage(note).folders, ({ key }) => key === note.folder)
|
// note.folder = folder key
|
||||||
|
return _.find(
|
||||||
|
this.getNoteStorage(note).folders,
|
||||||
|
({ key }) => key === note.folder
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewType() {
|
getViewType() {
|
||||||
@@ -1023,11 +1110,18 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { location, config, match: { params: { folderKey } } } = this.props
|
const {
|
||||||
|
location,
|
||||||
|
config,
|
||||||
|
match: {
|
||||||
|
params: { folderKey }
|
||||||
|
}
|
||||||
|
} = this.props
|
||||||
let { notes } = this.props
|
let { notes } = this.props
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
||||||
const sortFunc = sortBy === 'CREATED_AT'
|
const sortFunc =
|
||||||
|
sortBy === 'CREATED_AT'
|
||||||
? sortByCreatedAt
|
? sortByCreatedAt
|
||||||
: sortBy === 'ALPHABETICAL'
|
: sortBy === 'ALPHABETICAL'
|
||||||
? sortByAlphabetical
|
? sortByAlphabetical
|
||||||
@@ -1035,9 +1129,10 @@ class NoteList extends React.Component {
|
|||||||
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||||
? this.getNotes().sort(sortFunc)
|
? this.getNotes().sort(sortFunc)
|
||||||
: this.sortByPin(this.getNotes().sort(sortFunc))
|
: this.sortByPin(this.getNotes().sort(sortFunc))
|
||||||
this.notes = notes = sortedNotes.filter((note) => {
|
this.notes = notes = sortedNotes.filter(note => {
|
||||||
// this is for the trash box
|
// this is for the trash box
|
||||||
if (note.isTrashed !== true || location.pathname === '/trashed') return true
|
if (note.isTrashed !== true || location.pathname === '/trashed')
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
moment.updateLocale('en', {
|
moment.updateLocale('en', {
|
||||||
@@ -1066,8 +1161,7 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys.length === 0 ||
|
selectedNoteKeys.length === 0 ||
|
||||||
notes.every(note => !selectedNoteKeys.includes(note.key))
|
notes.every(note => !selectedNoteKeys.includes(note.key))
|
||||||
|
|
||||||
const noteList = notes
|
const noteList = notes.map((note, index) => {
|
||||||
.map((note, index) => {
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1080,8 +1174,7 @@ class NoteList extends React.Component {
|
|||||||
notes.length === 1 ||
|
notes.length === 1 ||
|
||||||
(autoSelectFirst && index === 0)
|
(autoSelectFirst && index === 0)
|
||||||
const dateDisplay = moment(
|
const dateDisplay = moment(
|
||||||
sortBy === 'CREATED_AT'
|
sortBy === 'CREATED_AT' ? note.createdAt : note.updatedAt
|
||||||
? note.createdAt : note.updatedAt
|
|
||||||
).fromNow('D')
|
).fromNow('D')
|
||||||
|
|
||||||
if (isDefault) {
|
if (isDefault) {
|
||||||
@@ -1121,47 +1214,62 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='NoteList'
|
<div
|
||||||
|
className='NoteList'
|
||||||
styleName='root'
|
styleName='root'
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
onDrop={(e) => this.handleDrop(e)}
|
onDrop={e => this.handleDrop(e)}
|
||||||
>
|
>
|
||||||
<div styleName='control'>
|
<div styleName='control'>
|
||||||
<div styleName='control-sortBy'>
|
<div styleName='control-sortBy'>
|
||||||
<i className='fa fa-angle-down' />
|
<i className='fa fa-angle-down' />
|
||||||
<select styleName='control-sortBy-select'
|
<select
|
||||||
|
styleName='control-sortBy-select'
|
||||||
title={i18n.__('Select filter mode')}
|
title={i18n.__('Select filter mode')}
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => this.handleSortByChange(e)}
|
onChange={e => this.handleSortByChange(e)}
|
||||||
>
|
>
|
||||||
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
<option title='Sort by update time' value='UPDATED_AT'>
|
||||||
<option title='Sort by create time' value='CREATED_AT'>{i18n.__('Created')}</option>
|
{i18n.__('Updated')}
|
||||||
<option title='Sort alphabetically' value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
|
</option>
|
||||||
|
<option title='Sort by create time' value='CREATED_AT'>
|
||||||
|
{i18n.__('Created')}
|
||||||
|
</option>
|
||||||
|
<option title='Sort alphabetically' value='ALPHABETICAL'>
|
||||||
|
{i18n.__('Alphabetically')}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='control-button-area'>
|
<div styleName='control-button-area'>
|
||||||
<button title={i18n.__('Default View')} styleName={config.listStyle === 'DEFAULT'
|
<button
|
||||||
|
title={i18n.__('Default View')}
|
||||||
|
styleName={
|
||||||
|
config.listStyle === 'DEFAULT'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
onClick={e => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
||||||
>
|
>
|
||||||
<img src='../resources/icon/icon-column.svg' />
|
<img src='../resources/icon/icon-column.svg' />
|
||||||
</button>
|
</button>
|
||||||
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
|
<button
|
||||||
|
title={i18n.__('Compressed View')}
|
||||||
|
styleName={
|
||||||
|
config.listStyle === 'SMALL'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
onClick={e => this.handleListStyleButtonClick(e, 'SMALL')}
|
||||||
>
|
>
|
||||||
<img src='../resources/icon/icon-column-list.svg' />
|
<img src='../resources/icon/icon-column-list.svg' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='list'
|
<div
|
||||||
|
styleName='list'
|
||||||
ref='list'
|
ref='list'
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
onKeyDown={e => this.handleNoteListKeyDown(e)}
|
||||||
onKeyUp={this.handleNoteListKeyUp}
|
onKeyUp={this.handleNoteListKeyUp}
|
||||||
onBlur={this.handleNoteListBlur}
|
onBlur={this.handleNoteListBlur}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './SwitchButton.styl'
|
import styles from './SwitchButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const ListButton = ({
|
const ListButton = ({ onClick, isTagActive }) => (
|
||||||
onClick, isTagActive
|
<button
|
||||||
}) => (
|
styleName={isTagActive ? 'non-active-button' : 'active-button'}
|
||||||
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={onClick}>
|
onClick={onClick}
|
||||||
<img src={isTagActive
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
isTagActive
|
||||||
? '../resources/icon/icon-list.svg'
|
? '../resources/icon/icon-list.svg'
|
||||||
: '../resources/icon/icon-list-active.svg'
|
: '../resources/icon/icon-list-active.svg'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './PreferenceButton.styl'
|
import styles from './PreferenceButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const PreferenceButton = ({
|
const PreferenceButton = ({ onClick }) => (
|
||||||
onClick
|
<button styleName='top-menu-preference' onClick={e => onClick(e)}>
|
||||||
}) => (
|
|
||||||
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
|
||||||
<img src='../resources/icon/icon-setting.svg' />
|
<img src='../resources/icon/icon-setting.svg' />
|
||||||
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class StorageItem extends React.Component {
|
|||||||
context.popup([
|
context.popup([
|
||||||
{
|
{
|
||||||
label: i18n.__('Add Folder'),
|
label: i18n.__('Add Folder'),
|
||||||
click: (e) => this.handleAddFolderButtonClick(e)
|
click: e => this.handleAddFolderButtonClick(e)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
@@ -44,11 +44,11 @@ class StorageItem extends React.Component {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: i18n.__('Export as txt'),
|
label: i18n.__('Export as txt'),
|
||||||
click: (e) => this.handleExportStorageClick(e, 'txt')
|
click: e => this.handleExportStorageClick(e, 'txt')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.__('Export as md'),
|
label: i18n.__('Export as md'),
|
||||||
click: (e) => this.handleExportStorageClick(e, 'md')
|
click: e => this.handleExportStorageClick(e, 'md')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -57,7 +57,7 @@ class StorageItem extends React.Component {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.__('Unlink Storage'),
|
label: i18n.__('Unlink Storage'),
|
||||||
click: (e) => this.handleUnlinkStorageClick(e)
|
click: e => this.handleUnlinkStorageClick(e)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -66,20 +66,23 @@ class StorageItem extends React.Component {
|
|||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Unlink Storage'),
|
message: i18n.__('Unlink Storage'),
|
||||||
detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'),
|
detail: i18n.__(
|
||||||
|
"This work will just detatches a storage from Boostnote. (Any data won't be deleted.)"
|
||||||
|
),
|
||||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
dataApi.removeStorage(storage.key)
|
dataApi
|
||||||
|
.removeStorage(storage.key)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'REMOVE_STORAGE',
|
type: 'REMOVE_STORAGE',
|
||||||
storageKey: storage.key
|
storageKey: storage.key
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -92,13 +95,10 @@ class StorageItem extends React.Component {
|
|||||||
title: i18n.__('Select a folder to export the files to'),
|
title: i18n.__('Select a folder to export the files to'),
|
||||||
multiSelections: false
|
multiSelections: false
|
||||||
}
|
}
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
||||||
(paths) => {
|
|
||||||
if (paths && paths.length === 1) {
|
if (paths && paths.length === 1) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
dataApi
|
dataApi.exportStorage(storage.key, fileType, paths[0]).then(data => {
|
||||||
.exportStorage(storage.key, fileType, paths[0])
|
|
||||||
.then(data => {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'EXPORT_STORAGE',
|
type: 'EXPORT_STORAGE',
|
||||||
storage: data.storage,
|
storage: data.storage,
|
||||||
@@ -112,8 +112,7 @@ class StorageItem extends React.Component {
|
|||||||
handleToggleButtonClick(e) {
|
handleToggleButtonClick(e) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
const isOpen = !this.state.isOpen
|
const isOpen = !this.state.isOpen
|
||||||
dataApi.toggleStorage(storage.key, isOpen)
|
dataApi.toggleStorage(storage.key, isOpen).then(storage => {
|
||||||
.then((storage) => {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'EXPAND_STORAGE',
|
type: 'EXPAND_STORAGE',
|
||||||
storage,
|
storage,
|
||||||
@@ -139,7 +138,7 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFolderButtonClick(folderKey) {
|
handleFolderButtonClick(folderKey) {
|
||||||
return (e) => {
|
return e => {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
|
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
|
||||||
}
|
}
|
||||||
@@ -149,7 +148,7 @@ class StorageItem extends React.Component {
|
|||||||
context.popup([
|
context.popup([
|
||||||
{
|
{
|
||||||
label: i18n.__('Rename Folder'),
|
label: i18n.__('Rename Folder'),
|
||||||
click: (e) => this.handleRenameFolderClick(e, folder)
|
click: e => this.handleRenameFolderClick(e, folder)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
@@ -159,11 +158,11 @@ class StorageItem extends React.Component {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: i18n.__('Export as txt'),
|
label: i18n.__('Export as txt'),
|
||||||
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
|
click: e => this.handleExportFolderClick(e, folder, 'txt')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.__('Export as md'),
|
label: i18n.__('Export as md'),
|
||||||
click: (e) => this.handleExportFolderClick(e, folder, 'md')
|
click: e => this.handleExportFolderClick(e, folder, 'md')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -172,7 +171,7 @@ class StorageItem extends React.Component {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.__('Delete Folder'),
|
label: i18n.__('Delete Folder'),
|
||||||
click: (e) => this.handleFolderDeleteClick(e, folder)
|
click: e => this.handleFolderDeleteClick(e, folder)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -192,13 +191,12 @@ class StorageItem extends React.Component {
|
|||||||
title: i18n.__('Select a folder to export the files to'),
|
title: i18n.__('Select a folder to export the files to'),
|
||||||
multiSelections: false
|
multiSelections: false
|
||||||
}
|
}
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
||||||
(paths) => {
|
|
||||||
if (paths && paths.length === 1) {
|
if (paths && paths.length === 1) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
dataApi
|
dataApi
|
||||||
.exportFolder(storage.key, folder.key, fileType, paths[0])
|
.exportFolder(storage.key, folder.key, fileType, paths[0])
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'EXPORT_FOLDER',
|
type: 'EXPORT_FOLDER',
|
||||||
storage: data.storage,
|
storage: data.storage,
|
||||||
@@ -228,15 +226,15 @@ class StorageItem extends React.Component {
|
|||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Delete Folder'),
|
message: i18n.__('Delete Folder'),
|
||||||
detail: i18n.__('This will delete all notes in the folder and can not be undone.'),
|
detail: i18n.__(
|
||||||
|
'This will delete all notes in the folder and can not be undone.'
|
||||||
|
),
|
||||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
dataApi
|
dataApi.deleteFolder(storage.key, folder.key).then(data => {
|
||||||
.deleteFolder(storage.key, folder.key)
|
|
||||||
.then((data) => {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'DELETE_FOLDER',
|
type: 'DELETE_FOLDER',
|
||||||
storage: data.storage,
|
storage: data.storage,
|
||||||
@@ -248,7 +246,9 @@ class StorageItem extends React.Component {
|
|||||||
|
|
||||||
handleDragEnter(e, key) {
|
handleDragEnter(e, key) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (this.state.draggedOver === key) { return }
|
if (this.state.draggedOver === key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
draggedOver: key
|
draggedOver: key
|
||||||
})
|
})
|
||||||
@@ -256,29 +256,35 @@ class StorageItem extends React.Component {
|
|||||||
|
|
||||||
handleDragLeave(e) {
|
handleDragLeave(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (this.state.draggedOver === null) { return }
|
if (this.state.draggedOver === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
draggedOver: null
|
draggedOver: null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
dropNote(storage, folder, dispatch, location, noteData) {
|
dropNote(storage, folder, dispatch, location, noteData) {
|
||||||
noteData = noteData.filter((note) => folder.key !== note.folder)
|
noteData = noteData.filter(note => folder.key !== note.folder)
|
||||||
if (noteData.length === 0) return
|
if (noteData.length === 0) return
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key))
|
noteData.map(note =>
|
||||||
|
dataApi.moveNote(note.storage, note.key, storage.key, folder.key)
|
||||||
)
|
)
|
||||||
.then((createdNoteData) => {
|
)
|
||||||
createdNoteData.forEach((newNote) => {
|
.then(createdNoteData => {
|
||||||
|
createdNoteData.forEach(newNote => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'MOVE_NOTE',
|
type: 'MOVE_NOTE',
|
||||||
originNote: noteData.find((note) => note.content === newNote.oldContent),
|
originNote: noteData.find(
|
||||||
|
note => note.content === newNote.oldContent
|
||||||
|
),
|
||||||
note: newNote
|
note: newNote
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
console.error(`error on delete notes: ${err}`)
|
console.error(`error on delete notes: ${err}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -299,16 +305,32 @@ class StorageItem extends React.Component {
|
|||||||
const { folderNoteMap, trashedSet } = data
|
const { folderNoteMap, trashedSet } = data
|
||||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||||
const folderList = storage.folders.map((folder, index) => {
|
const folderList = storage.folders.map((folder, index) => {
|
||||||
const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
const folderRegex = new RegExp(
|
||||||
const isActive = !!(location.pathname.match(folderRegex))
|
escapeStringRegexp(path.sep) +
|
||||||
|
'storages' +
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
storage.key +
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
'folders' +
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
folder.key
|
||||||
|
)
|
||||||
|
const isActive = !!location.pathname.match(folderRegex)
|
||||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||||
|
|
||||||
let noteCount = 0
|
let noteCount = 0
|
||||||
if (noteSet) {
|
if (noteSet) {
|
||||||
let trashedNoteCount = 0
|
let trashedNoteCount = 0
|
||||||
const noteKeys = noteSet.map(noteKey => { return noteKey })
|
const noteKeys = noteSet.map(noteKey => {
|
||||||
|
return noteKey
|
||||||
|
})
|
||||||
trashedSet.toJS().forEach(trashedKey => {
|
trashedSet.toJS().forEach(trashedKey => {
|
||||||
if (noteKeys.some(noteKey => { return noteKey === trashedKey })) trashedNoteCount++
|
if (
|
||||||
|
noteKeys.some(noteKey => {
|
||||||
|
return noteKey === trashedKey
|
||||||
|
})
|
||||||
|
)
|
||||||
|
trashedNoteCount++
|
||||||
})
|
})
|
||||||
noteCount = noteSet.size - trashedNoteCount
|
noteCount = noteSet.size - trashedNoteCount
|
||||||
}
|
}
|
||||||
@@ -317,73 +339,80 @@ class StorageItem extends React.Component {
|
|||||||
key={folder.key}
|
key={folder.key}
|
||||||
index={index}
|
index={index}
|
||||||
isActive={isActive || folder.key === this.state.draggedOver}
|
isActive={isActive || folder.key === this.state.draggedOver}
|
||||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
handleButtonClick={e => this.handleFolderButtonClick(folder.key)(e)}
|
||||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
handleContextMenu={e => this.handleFolderButtonContextMenu(e, folder)}
|
||||||
folderName={folder.name}
|
folderName={folder.name}
|
||||||
folderColor={folder.color}
|
folderColor={folder.color}
|
||||||
isFolded={isFolded}
|
isFolded={isFolded}
|
||||||
noteCount={noteCount}
|
noteCount={noteCount}
|
||||||
handleDrop={(e) => {
|
handleDrop={e => {
|
||||||
this.handleDrop(e, storage, folder, dispatch, location)
|
this.handleDrop(e, storage, folder, dispatch, location)
|
||||||
}}
|
}}
|
||||||
handleDragEnter={(e) => {
|
handleDragEnter={e => {
|
||||||
this.handleDragEnter(e, folder.key)
|
this.handleDragEnter(e, folder.key)
|
||||||
}}
|
}}
|
||||||
handleDragLeave={(e) => {
|
handleDragLeave={e => {
|
||||||
this.handleDragLeave(e, folder)
|
this.handleDragLeave(e, folder)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isActive = location.pathname.match(new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + '$'))
|
const isActive = location.pathname.match(
|
||||||
|
new RegExp(
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
'storages' +
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
storage.key +
|
||||||
|
'$'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div styleName={isFolded ? 'root--folded' : 'root'}
|
<div styleName={isFolded ? 'root--folded' : 'root'} key={storage.key}>
|
||||||
key={storage.key}
|
<div
|
||||||
|
styleName={isActive ? 'header--active' : 'header'}
|
||||||
|
onContextMenu={e => this.handleHeaderContextMenu(e)}
|
||||||
>
|
>
|
||||||
<div styleName={isActive
|
<button
|
||||||
? 'header--active'
|
styleName='header-toggleButton'
|
||||||
: 'header'
|
onMouseDown={e => this.handleToggleButtonClick(e)}
|
||||||
}
|
|
||||||
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
|
|
||||||
>
|
>
|
||||||
<button styleName='header-toggleButton'
|
<img
|
||||||
onMouseDown={(e) => this.handleToggleButtonClick(e)}
|
src={
|
||||||
>
|
this.state.isOpen
|
||||||
<img src={this.state.isOpen
|
|
||||||
? '../resources/icon/icon-down.svg'
|
? '../resources/icon/icon-down.svg'
|
||||||
: '../resources/icon/icon-right.svg'
|
: '../resources/icon/icon-right.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{!isFolded &&
|
{!isFolded && (
|
||||||
<button styleName='header-addFolderButton'
|
<button
|
||||||
onClick={(e) => this.handleAddFolderButtonClick(e)}
|
styleName='header-addFolderButton'
|
||||||
|
onClick={e => this.handleAddFolderButtonClick(e)}
|
||||||
>
|
>
|
||||||
<img src='../resources/icon/icon-plus.svg' />
|
<img src='../resources/icon/icon-plus.svg' />
|
||||||
</button>
|
</button>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<button styleName='header-info'
|
<button
|
||||||
onClick={(e) => this.handleHeaderInfoClick(e)}
|
styleName='header-info'
|
||||||
|
onClick={e => this.handleHeaderInfoClick(e)}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
|
{isFolded
|
||||||
|
? _.truncate(storage.name, { length: 1, omission: '' })
|
||||||
|
: storage.name}
|
||||||
</span>
|
</span>
|
||||||
{isFolded &&
|
{isFolded && (
|
||||||
<span styleName='header-info--folded-tooltip'>
|
<span styleName='header-info--folded-tooltip'>
|
||||||
{storage.name}
|
{storage.name}
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{this.state.isOpen &&
|
{this.state.isOpen && <div>{folderList}</div>}
|
||||||
<div>
|
|
||||||
{folderList}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './SwitchButton.styl'
|
import styles from './SwitchButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const TagButton = ({
|
const TagButton = ({ onClick, isTagActive }) => (
|
||||||
onClick, isTagActive
|
<button
|
||||||
}) => (
|
styleName={isTagActive ? 'active-button' : 'non-active-button'}
|
||||||
<button styleName={isTagActive ? 'active-button' : 'non-active-button'} onClick={onClick}>
|
onClick={onClick}
|
||||||
<img src={isTagActive
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
isTagActive
|
||||||
? '../resources/icon/icon-tag-active.svg'
|
? '../resources/icon/icon-tag-active.svg'
|
||||||
: '../resources/icon/icon-tag.svg'
|
: '../resources/icon/icon-tag.svg'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,15 +56,23 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteTag(tag) {
|
deleteTag(tag) {
|
||||||
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
const selectedButton = remote.dialog.showMessageBox(
|
||||||
|
remote.getCurrentWindow(),
|
||||||
|
{
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Confirm tag deletion'),
|
message: i18n.__('Confirm tag deletion'),
|
||||||
detail: i18n.__('This will permanently remove this tag.'),
|
detail: i18n.__('This will permanently remove this tag.'),
|
||||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (selectedButton === 0) {
|
if (selectedButton === 0) {
|
||||||
const { data, dispatch, location, match: { params } } = this.props
|
const {
|
||||||
|
data,
|
||||||
|
dispatch,
|
||||||
|
location,
|
||||||
|
match: { params }
|
||||||
|
} = this.props
|
||||||
|
|
||||||
const notes = data.noteMap
|
const notes = data.noteMap
|
||||||
.map(note => note)
|
.map(note => note)
|
||||||
@@ -78,9 +86,9 @@ class SideNav extends React.Component {
|
|||||||
return note
|
return note
|
||||||
})
|
})
|
||||||
|
|
||||||
Promise
|
Promise.all(
|
||||||
.all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
|
notes.map(note => dataApi.updateNote(note.storage, note.key, note))
|
||||||
.then(updatedNotes => {
|
).then(updatedNotes => {
|
||||||
updatedNotes.forEach(note => {
|
updatedNotes.forEach(note => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
@@ -94,7 +102,11 @@ class SideNav extends React.Component {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tags.splice(index, 1)
|
tags.splice(index, 1)
|
||||||
|
|
||||||
dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`))
|
dispatch(
|
||||||
|
push(
|
||||||
|
`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -125,7 +137,11 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
menu.push({
|
menu.push({
|
||||||
label: i18n.__('Customize Color'),
|
label: i18n.__('Customize Color'),
|
||||||
click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect())
|
click: this.displayColorPicker.bind(
|
||||||
|
this,
|
||||||
|
tag,
|
||||||
|
e.target.getBoundingClientRect()
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
context.popup(menu)
|
context.popup(menu)
|
||||||
@@ -152,9 +168,16 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleColorPickerConfirm(color) {
|
handleColorPickerConfirm(color) {
|
||||||
const { dispatch, config: {coloredTags} } = this.props
|
const {
|
||||||
const { colorPicker: { tagName } } = this.state
|
dispatch,
|
||||||
const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex})
|
config: { coloredTags }
|
||||||
|
} = this.props
|
||||||
|
const {
|
||||||
|
colorPicker: { tagName }
|
||||||
|
} = this.state
|
||||||
|
const newColoredTags = Object.assign({}, coloredTags, {
|
||||||
|
[tagName]: color.hex
|
||||||
|
})
|
||||||
|
|
||||||
const config = { coloredTags: newColoredTags }
|
const config = { coloredTags: newColoredTags }
|
||||||
ConfigManager.set(config)
|
ConfigManager.set(config)
|
||||||
@@ -166,8 +189,13 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleColorPickerReset() {
|
handleColorPickerReset() {
|
||||||
const { dispatch, config: {coloredTags} } = this.props
|
const {
|
||||||
const { colorPicker: { tagName } } = this.state
|
dispatch,
|
||||||
|
config: { coloredTags }
|
||||||
|
} = this.props
|
||||||
|
const {
|
||||||
|
colorPicker: { tagName }
|
||||||
|
} = this.state
|
||||||
const newColoredTags = Object.assign({}, coloredTags)
|
const newColoredTags = Object.assign({}, coloredTags)
|
||||||
|
|
||||||
delete newColoredTags[tagName]
|
delete newColoredTags[tagName]
|
||||||
@@ -209,9 +237,7 @@ class SideNav extends React.Component {
|
|||||||
onSortEnd(storage) {
|
onSortEnd(storage) {
|
||||||
return ({ oldIndex, newIndex }) => {
|
return ({ oldIndex, newIndex }) => {
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
dataApi
|
dataApi.reorderFolder(storage.key, oldIndex, newIndex).then(data => {
|
||||||
.reorderFolder(storage.key, oldIndex, newIndex)
|
|
||||||
.then((data) => {
|
|
||||||
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
|
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -227,25 +253,35 @@ class SideNav extends React.Component {
|
|||||||
let component
|
let component
|
||||||
|
|
||||||
// TagsMode is not selected
|
// TagsMode is not selected
|
||||||
if (!location.pathname.match('/tags') && !location.pathname.match('/alltags')) {
|
if (
|
||||||
|
!location.pathname.match('/tags') &&
|
||||||
|
!location.pathname.match('/alltags')
|
||||||
|
) {
|
||||||
component = (
|
component = (
|
||||||
<div>
|
<div>
|
||||||
<SideNavFilter
|
<SideNavFilter
|
||||||
isFolded={isFolded}
|
isFolded={isFolded}
|
||||||
isHomeActive={isHomeActive}
|
isHomeActive={isHomeActive}
|
||||||
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
|
handleAllNotesButtonClick={e => this.handleHomeButtonClick(e)}
|
||||||
isStarredActive={isStarredActive}
|
isStarredActive={isStarredActive}
|
||||||
isTrashedActive={isTrashedActive}
|
isTrashedActive={isTrashedActive}
|
||||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
handleStarredButtonClick={e => this.handleStarredButtonClick(e)}
|
||||||
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
handleTrashedButtonClick={e => this.handleTrashedButtonClick(e)}
|
||||||
counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
|
counterTotalNote={
|
||||||
|
data.noteMap._map.size - data.trashedSet._set.size
|
||||||
|
}
|
||||||
counterStarredNote={data.starredSet._set.size}
|
counterStarredNote={data.starredSet._set.size}
|
||||||
counterDelNote={data.trashedSet._set.size}
|
counterDelNote={data.trashedSet._set.size}
|
||||||
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
|
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(
|
||||||
|
this
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StorageList storageList={storageList} isFolded={isFolded} />
|
<StorageList storageList={storageList} isFolded={isFolded} />
|
||||||
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
|
<NavToggleButton
|
||||||
|
isFolded={isFolded}
|
||||||
|
handleToggleButtonClick={this.handleToggleButtonClick.bind(this)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -257,22 +293,26 @@ class SideNav extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div styleName='tag-control-sortTagsBy'>
|
<div styleName='tag-control-sortTagsBy'>
|
||||||
<i className='fa fa-angle-down' />
|
<i className='fa fa-angle-down' />
|
||||||
<select styleName='tag-control-sortTagsBy-select'
|
<select
|
||||||
|
styleName='tag-control-sortTagsBy-select'
|
||||||
title={i18n.__('Select filter mode')}
|
title={i18n.__('Select filter mode')}
|
||||||
value={config.sortTagsBy}
|
value={config.sortTagsBy}
|
||||||
onChange={(e) => this.handleSortTagsByChange(e)}
|
onChange={e => this.handleSortTagsByChange(e)}
|
||||||
>
|
>
|
||||||
<option title='Sort alphabetically'
|
<option title='Sort alphabetically' value='ALPHABETICAL'>
|
||||||
value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
|
{i18n.__('Alphabetically')}
|
||||||
<option title='Sort by update time'
|
</option>
|
||||||
value='COUNTER'>{i18n.__('Counter')}</option>
|
<option title='Sort by update time' value='COUNTER'>
|
||||||
|
{i18n.__('Counter')}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='tagList'>
|
<div styleName='tagList'>{this.tagListComponent(data)}</div>
|
||||||
{this.tagListComponent(data)}
|
<NavToggleButton
|
||||||
</div>
|
isFolded={isFolded}
|
||||||
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
|
handleToggleButtonClick={this.handleToggleButtonClick.bind(this)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -285,35 +325,42 @@ class SideNav extends React.Component {
|
|||||||
const { colorPicker } = this.state
|
const { colorPicker } = this.state
|
||||||
const activeTags = this.getActiveTags(location.pathname)
|
const activeTags = this.getActiveTags(location.pathname)
|
||||||
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
||||||
let tagList = sortBy(data.tagNoteMap.map(
|
let tagList = sortBy(
|
||||||
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
data.tagNoteMap
|
||||||
).filter(
|
.map((tag, name) => ({
|
||||||
tag => tag.size > 0
|
name,
|
||||||
), ['name'])
|
size: tag.size,
|
||||||
|
related: relatedTags.has(name)
|
||||||
|
}))
|
||||||
|
.filter(tag => tag.size > 0),
|
||||||
|
['name']
|
||||||
|
)
|
||||||
if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
|
if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
|
||||||
const notesTags = data.noteMap.map(note => note.tags)
|
const notesTags = data.noteMap.map(note => note.tags)
|
||||||
tagList = tagList.map(tag => {
|
tagList = tagList.map(tag => {
|
||||||
tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length
|
tag.size = notesTags.filter(
|
||||||
|
tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)
|
||||||
|
).length
|
||||||
return tag
|
return tag
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (config.sortTagsBy === 'COUNTER') {
|
if (config.sortTagsBy === 'COUNTER') {
|
||||||
tagList = sortBy(tagList, item => (0 - item.size))
|
tagList = sortBy(tagList, item => 0 - item.size)
|
||||||
}
|
}
|
||||||
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
|
if (config.ui.showOnlyRelatedTags && relatedTags.size > 0) {
|
||||||
tagList = tagList.filter(
|
tagList = tagList.filter(tag => tag.related)
|
||||||
tag => tag.related
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return (
|
return tagList.map(tag => {
|
||||||
tagList.map(tag => {
|
|
||||||
return (
|
return (
|
||||||
<TagListItem
|
<TagListItem
|
||||||
name={tag.name}
|
name={tag.name}
|
||||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||||
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||||
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
||||||
isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)}
|
isActive={
|
||||||
|
this.getTagActive(location.pathname, tag.name) ||
|
||||||
|
colorPicker.tagName === tag.name
|
||||||
|
}
|
||||||
isRelated={tag.related}
|
isRelated={tag.related}
|
||||||
key={tag.name}
|
key={tag.name}
|
||||||
count={tag.size}
|
count={tag.size}
|
||||||
@@ -321,18 +368,15 @@ class SideNav extends React.Component {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getRelatedTags(activeTags, noteMap) {
|
getRelatedTags(activeTags, noteMap) {
|
||||||
if (activeTags.length === 0) {
|
if (activeTags.length === 0) {
|
||||||
return new Set()
|
return new Set()
|
||||||
}
|
}
|
||||||
const relatedNotes = noteMap.map(
|
const relatedNotes = noteMap
|
||||||
note => ({key: note.key, tags: note.tags})
|
.map(note => ({ key: note.key, tags: note.tags }))
|
||||||
).filter(
|
.filter(note => activeTags.every(tag => note.tags.includes(tag)))
|
||||||
note => activeTags.every(tag => note.tags.includes(tag))
|
|
||||||
)
|
|
||||||
const relatedTags = new Set()
|
const relatedTags = new Set()
|
||||||
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
|
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
|
||||||
return relatedTags
|
return relatedTags
|
||||||
@@ -345,9 +389,7 @@ class SideNav extends React.Component {
|
|||||||
getActiveTags(path) {
|
getActiveTags(path) {
|
||||||
const pathSegments = path.split('/')
|
const pathSegments = path.split('/')
|
||||||
const tags = pathSegments[pathSegments.length - 1]
|
const tags = pathSegments[pathSegments.length - 1]
|
||||||
return (tags === 'alltags')
|
return tags === 'alltags' ? [] : decodeURIComponent(tags).split(' ')
|
||||||
? []
|
|
||||||
: decodeURIComponent(tags).split(' ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickTagListItem(name) {
|
handleClickTagListItem(name) {
|
||||||
@@ -383,27 +425,32 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
emptyTrash(entries) {
|
emptyTrash(entries) {
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
const deletionPromises = entries.map((note) => {
|
const deletionPromises = entries.map(note => {
|
||||||
return dataApi.deleteNote(note.storage, note.key)
|
return dataApi.deleteNote(note.storage, note.key)
|
||||||
})
|
})
|
||||||
const { confirmDeletion } = this.props.config.ui
|
const { confirmDeletion } = this.props.config.ui
|
||||||
if (!confirmDeleteNote(confirmDeletion, true)) return
|
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||||
Promise.all(deletionPromises)
|
Promise.all(deletionPromises)
|
||||||
.then((arrayOfStorageAndNoteKeys) => {
|
.then(arrayOfStorageAndNoteKeys => {
|
||||||
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||||
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
|
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
console.error('Cannot Delete note: ' + err)
|
console.error('Cannot Delete note: ' + err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFilterButtonContextMenu(event) {
|
handleFilterButtonContextMenu(event) {
|
||||||
const { data } = this.props
|
const { data } = this.props
|
||||||
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
const trashedNotes = data.trashedSet
|
||||||
|
.toJS()
|
||||||
|
.map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||||
context.popup([
|
context.popup([
|
||||||
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
|
{
|
||||||
|
label: i18n.__('Empty Trash'),
|
||||||
|
click: () => this.emptyTrash(trashedNotes)
|
||||||
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +462,8 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
const storageList = data.storageMap.map((storage, key) => {
|
const storageList = data.storageMap.map((storage, key) => {
|
||||||
const SortableStorageItem = SortableContainer(StorageItem)
|
const SortableStorageItem = SortableContainer(StorageItem)
|
||||||
return <SortableStorageItem
|
return (
|
||||||
|
<SortableStorageItem
|
||||||
key={storage.key}
|
key={storage.key}
|
||||||
storage={storage}
|
storage={storage}
|
||||||
data={data}
|
data={data}
|
||||||
@@ -425,6 +473,7 @@ class SideNav extends React.Component {
|
|||||||
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
||||||
useDragHandle
|
useDragHandle
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
let colorPicker
|
let colorPicker
|
||||||
@@ -444,15 +493,22 @@ class SideNav extends React.Component {
|
|||||||
if (!isFolded) style.width = this.props.width
|
if (!isFolded) style.width = this.props.width
|
||||||
const isTagActive = /tag/.test(location.pathname)
|
const isTagActive = /tag/.test(location.pathname)
|
||||||
return (
|
return (
|
||||||
<div className='SideNav'
|
<div
|
||||||
|
className='SideNav'
|
||||||
styleName={isFolded ? 'root--folded' : 'root'}
|
styleName={isFolded ? 'root--folded' : 'root'}
|
||||||
tabIndex='1'
|
tabIndex='1'
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<div styleName='top'>
|
<div styleName='top'>
|
||||||
<div styleName='switch-buttons'>
|
<div styleName='switch-buttons'>
|
||||||
<ListButton onClick={this.handleSwitchFoldersButtonClick.bind(this)} isTagActive={isTagActive} />
|
<ListButton
|
||||||
<TagButton onClick={this.handleSwitchTagsButtonClick.bind(this)} isTagActive={isTagActive} />
|
onClick={this.handleSwitchFoldersButtonClick.bind(this)}
|
||||||
|
isTagActive={isTagActive}
|
||||||
|
/>
|
||||||
|
<TagButton
|
||||||
|
onClick={this.handleSwitchTagsButtonClick.bind(this)}
|
||||||
|
isTagActive={isTagActive}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<PreferenceButton onClick={this.handleMenuButtonClick} />
|
<PreferenceButton onClick={this.handleMenuButtonClick} />
|
||||||
|
|||||||
@@ -11,10 +11,23 @@ const electron = require('electron')
|
|||||||
const { remote, ipcRenderer } = electron
|
const { remote, ipcRenderer } = electron
|
||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
|
|
||||||
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
const zoomOptions = [
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1,
|
||||||
|
1.1,
|
||||||
|
1.2,
|
||||||
|
1.3,
|
||||||
|
1.4,
|
||||||
|
1.5,
|
||||||
|
1.6,
|
||||||
|
1.7,
|
||||||
|
1.8,
|
||||||
|
1.9,
|
||||||
|
2.0
|
||||||
|
]
|
||||||
|
|
||||||
class StatusBar extends React.Component {
|
class StatusBar extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
|
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
|
||||||
@@ -50,7 +63,7 @@ class StatusBar extends React.Component {
|
|||||||
handleZoomButtonClick(e) {
|
handleZoomButtonClick(e) {
|
||||||
const templates = []
|
const templates = []
|
||||||
|
|
||||||
zoomOptions.forEach((zoom) => {
|
zoomOptions.forEach(zoom => {
|
||||||
templates.push({
|
templates.push({
|
||||||
label: Math.floor(zoom * 100) + '%',
|
label: Math.floor(zoom * 100) + '%',
|
||||||
click: () => this.handleZoomMenuItemClick(zoom)
|
click: () => this.handleZoomMenuItemClick(zoom)
|
||||||
@@ -87,22 +100,18 @@ class StatusBar extends React.Component {
|
|||||||
const { config, status } = this.context
|
const { config, status } = this.context
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='StatusBar'
|
<div className='StatusBar' styleName='root'>
|
||||||
styleName='root'
|
<button styleName='zoom' onClick={e => this.handleZoomButtonClick(e)}>
|
||||||
>
|
|
||||||
<button styleName='zoom'
|
|
||||||
onClick={(e) => this.handleZoomButtonClick(e)}
|
|
||||||
>
|
|
||||||
<img src='../resources/icon/icon-zoom.svg' />
|
<img src='../resources/icon/icon-zoom.svg' />
|
||||||
<span>{Math.floor(config.zoom * 100)}%</span>
|
<span>{Math.floor(config.zoom * 100)}%</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{status.updateReady
|
{status.updateReady ? (
|
||||||
? <button onClick={this.updateApp} styleName='update'>
|
<button onClick={this.updateApp} styleName='update'>
|
||||||
<i styleName='update-icon' className='fa fa-cloud-download' /> {i18n.__('Ready to Update!')}
|
<i styleName='update-icon' className='fa fa-cloud-download' />{' '}
|
||||||
|
{i18n.__('Ready to Update!')}
|
||||||
</button>
|
</button>
|
||||||
: null
|
) : null}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,19 +33,25 @@ class TopBar extends React.Component {
|
|||||||
this.handleSearchChange = this.handleSearchChange.bind(this)
|
this.handleSearchChange = this.handleSearchChange.bind(this)
|
||||||
this.handleSearchClearButton = this.handleSearchClearButton.bind(this)
|
this.handleSearchClearButton = this.handleSearchClearButton.bind(this)
|
||||||
|
|
||||||
this.debouncedUpdateKeyword = debounce((keyword) => {
|
this.debouncedUpdateKeyword = debounce(
|
||||||
|
keyword => {
|
||||||
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
|
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
|
||||||
this.setState({
|
this.setState({
|
||||||
search: keyword
|
search: keyword
|
||||||
})
|
})
|
||||||
ee.emit('top:search', keyword)
|
ee.emit('top:search', keyword)
|
||||||
}, 1000 / 60, {
|
},
|
||||||
|
1000 / 60,
|
||||||
|
{
|
||||||
maxWait: 1000 / 8
|
maxWait: 1000 / 8
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { match: { params } } = this.props
|
const {
|
||||||
|
match: { params }
|
||||||
|
} = this.props
|
||||||
const searchWord = params && params.searchword
|
const searchWord = params && params.searchword
|
||||||
if (searchWord !== undefined) {
|
if (searchWord !== undefined) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -144,13 +150,15 @@ class TopBar extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { config, style, location } = this.props
|
const { config, style, location } = this.props
|
||||||
return (
|
return (
|
||||||
<div className='TopBar'
|
<div
|
||||||
|
className='TopBar'
|
||||||
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
|
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<div styleName='control'>
|
<div styleName='control'>
|
||||||
<div styleName='control-search'>
|
<div styleName='control-search'>
|
||||||
<div styleName='control-search-input'
|
<div
|
||||||
|
styleName='control-search-input'
|
||||||
onFocus={this.handleSearchFocus}
|
onFocus={this.handleSearchFocus}
|
||||||
onBlur={this.handleSearchBlur}
|
onBlur={this.handleSearchBlur}
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
@@ -165,19 +173,24 @@ class TopBar extends React.Component {
|
|||||||
type='text'
|
type='text'
|
||||||
className='searchInput'
|
className='searchInput'
|
||||||
/>
|
/>
|
||||||
{this.state.search !== '' &&
|
{this.state.search !== '' && (
|
||||||
<button styleName='control-search-input-clear'
|
<button
|
||||||
|
styleName='control-search-input-clear'
|
||||||
onClick={this.handleSearchClearButton}
|
onClick={this.handleSearchClearButton}
|
||||||
>
|
>
|
||||||
<i className='fa fa-fw fa-times' />
|
<i className='fa fa-fw fa-times' />
|
||||||
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
|
<span styleName='control-search-input-clear-tooltip'>
|
||||||
|
{i18n.__('Clear Search')}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{location.pathname === '/trashed' ? ''
|
{location.pathname === '/trashed' ? (
|
||||||
: <NewNoteButton
|
''
|
||||||
|
) : (
|
||||||
|
<NewNoteButton
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
@@ -185,7 +198,8 @@ class TopBar extends React.Component {
|
|||||||
'location',
|
'location',
|
||||||
'match'
|
'match'
|
||||||
])}
|
])}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,12 +67,11 @@ document.addEventListener('keyup', function (e) {
|
|||||||
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
const className = e.target.className
|
const className = e.target.className
|
||||||
if (!className && typeof (className) !== 'string') return
|
if (!className && typeof className !== 'string') return
|
||||||
const isInfoButton = className.includes('infoButton')
|
const isInfoButton = className.includes('infoButton')
|
||||||
const offsetParent = e.target.offsetParent
|
const offsetParent = e.target.offsetParent
|
||||||
const isInfoPanel = offsetParent !== null
|
const isInfoPanel =
|
||||||
? offsetParent.className.includes('infoPanel')
|
offsetParent !== null ? offsetParent.className.includes('infoPanel') : false
|
||||||
: false
|
|
||||||
if (isInfoButton || isInfoPanel) return
|
if (isInfoButton || isInfoPanel) return
|
||||||
const infoPanel = document.querySelector('.infoPanel')
|
const infoPanel = document.querySelector('.infoPanel')
|
||||||
if (infoPanel) infoPanel.style.display = 'none'
|
if (infoPanel) infoPanel.style.display = 'none'
|
||||||
@@ -97,7 +96,7 @@ function updateApp () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render((
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -112,13 +111,17 @@ ReactDOM.render((
|
|||||||
{/* storages */}
|
{/* storages */}
|
||||||
<Redirect path='/storages' to='/home' exact />
|
<Redirect path='/storages' to='/home' exact />
|
||||||
<Route path='/storages/:storageKey' component={Main} exact />
|
<Route path='/storages/:storageKey' component={Main} exact />
|
||||||
<Route path='/storages/:storageKey/folders/:folderKey' component={Main} />
|
<Route
|
||||||
|
path='/storages/:storageKey/folders/:folderKey'
|
||||||
|
component={Main}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
<DevTools />
|
<DevTools />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>,
|
||||||
), el, function () {
|
el,
|
||||||
|
function() {
|
||||||
const loadingCover = document.getElementById('loadingCover')
|
const loadingCover = document.getElementById('loadingCover')
|
||||||
loadingCover.parentNode.removeChild(loadingCover)
|
loadingCover.parentNode.removeChild(loadingCover)
|
||||||
|
|
||||||
@@ -144,4 +147,5 @@ ReactDOM.render((
|
|||||||
ipcRenderer.send('update-check', 'check-update')
|
ipcRenderer.send('update-check', 'check-update')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function getSendEventCond () {
|
|||||||
|
|
||||||
function initAwsMobileAnalytics() {
|
function initAwsMobileAnalytics() {
|
||||||
if (getSendEventCond()) return
|
if (getSendEventCond()) return
|
||||||
AWS.config.credentials.get((err) => {
|
AWS.config.credentials.get(err => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
recordDynamicCustomEvent('APP_STARTED')
|
recordDynamicCustomEvent('APP_STARTED')
|
||||||
recordStaticCustomEvent()
|
recordStaticCustomEvent()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function bind (name, el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function release(el) {
|
function release(el) {
|
||||||
callees = callees.filter((callee) => callee.element !== el)
|
callees = callees.filter(callee => callee.element !== el)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fire(command) {
|
function fire(command) {
|
||||||
@@ -16,10 +16,9 @@ function fire (command) {
|
|||||||
const splitted = command.split(':')
|
const splitted = command.split(':')
|
||||||
const target = splitted[0]
|
const target = splitted[0]
|
||||||
const targetCommand = splitted[1]
|
const targetCommand = splitted[1]
|
||||||
const targetCallees = callees
|
const targetCallees = callees.filter(callee => callee.name === target)
|
||||||
.filter((callee) => callee.name === target)
|
|
||||||
|
|
||||||
targetCallees.forEach((callee) => {
|
targetCallees.forEach(callee => {
|
||||||
callee.element.fire(targetCommand)
|
callee.element.fire(targetCommand)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export const DEFAULT_CONFIG = {
|
|||||||
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
||||||
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
||||||
toggleDirection: OSX ? 'Command + Alt + Right' : 'Ctrl + Right',
|
toggleDirection: OSX ? 'Command + Alt + Right' : 'Ctrl + Right',
|
||||||
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
|
deleteNote: OSX
|
||||||
|
? 'Command + Shift + Backspace'
|
||||||
|
: 'Ctrl + Shift + Backspace',
|
||||||
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
|
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
|
||||||
prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F',
|
prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F',
|
||||||
sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S',
|
sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S',
|
||||||
@@ -131,7 +133,11 @@ function _save (config) {
|
|||||||
|
|
||||||
function get() {
|
function get() {
|
||||||
const rawStoredConfig = window.localStorage.getItem('config')
|
const rawStoredConfig = window.localStorage.getItem('config')
|
||||||
const storedConfig = Object.assign({}, DEFAULT_CONFIG, JSON.parse(rawStoredConfig))
|
const storedConfig = Object.assign(
|
||||||
|
{},
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
JSON.parse(rawStoredConfig)
|
||||||
|
)
|
||||||
let config = storedConfig
|
let config = storedConfig
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -145,7 +151,10 @@ function get () {
|
|||||||
_save(config)
|
_save(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.autoUpdateEnabled = electronConfig.get('autoUpdateEnabled', config.autoUpdateEnabled)
|
config.autoUpdateEnabled = electronConfig.get(
|
||||||
|
'autoUpdateEnabled',
|
||||||
|
config.autoUpdateEnabled
|
||||||
|
)
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
@@ -157,7 +166,9 @@ function get () {
|
|||||||
document.head.appendChild(editorTheme)
|
document.head.appendChild(editorTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = consts.THEMES.find(theme => theme.name === config.editor.theme)
|
const theme = consts.THEMES.find(
|
||||||
|
theme => theme.name === config.editor.theme
|
||||||
|
)
|
||||||
|
|
||||||
if (theme) {
|
if (theme) {
|
||||||
editorTheme.setAttribute('href', theme.path)
|
editorTheme.setAttribute('href', theme.path)
|
||||||
@@ -177,7 +188,12 @@ function set (updates) {
|
|||||||
arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS
|
arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS
|
||||||
}
|
}
|
||||||
|
|
||||||
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, arrangedUpdates)
|
const newConfig = Object.assign(
|
||||||
|
{},
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
currentConfig,
|
||||||
|
arrangedUpdates
|
||||||
|
)
|
||||||
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
|
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
|
||||||
_save(newConfig)
|
_save(newConfig)
|
||||||
|
|
||||||
@@ -197,7 +213,9 @@ function set (updates) {
|
|||||||
document.head.appendChild(editorTheme)
|
document.head.appendChild(editorTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme)
|
const newTheme = consts.THEMES.find(
|
||||||
|
theme => theme.name === newConfig.editor.theme
|
||||||
|
)
|
||||||
|
|
||||||
if (newTheme) {
|
if (newTheme) {
|
||||||
editorTheme.setAttribute('href', newTheme.path)
|
editorTheme.setAttribute('href', newTheme.path)
|
||||||
@@ -213,11 +231,36 @@ function set (updates) {
|
|||||||
|
|
||||||
function assignConfigValues(originalConfig, rcConfig) {
|
function assignConfigValues(originalConfig, rcConfig) {
|
||||||
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
|
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
|
||||||
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
|
config.hotkey = Object.assign(
|
||||||
config.blog = Object.assign({}, DEFAULT_CONFIG.blog, originalConfig.blog, rcConfig.blog)
|
{},
|
||||||
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
|
DEFAULT_CONFIG.hotkey,
|
||||||
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
originalConfig.hotkey,
|
||||||
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
rcConfig.hotkey
|
||||||
|
)
|
||||||
|
config.blog = Object.assign(
|
||||||
|
{},
|
||||||
|
DEFAULT_CONFIG.blog,
|
||||||
|
originalConfig.blog,
|
||||||
|
rcConfig.blog
|
||||||
|
)
|
||||||
|
config.ui = Object.assign(
|
||||||
|
{},
|
||||||
|
DEFAULT_CONFIG.ui,
|
||||||
|
originalConfig.ui,
|
||||||
|
rcConfig.ui
|
||||||
|
)
|
||||||
|
config.editor = Object.assign(
|
||||||
|
{},
|
||||||
|
DEFAULT_CONFIG.editor,
|
||||||
|
originalConfig.editor,
|
||||||
|
rcConfig.editor
|
||||||
|
)
|
||||||
|
config.preview = Object.assign(
|
||||||
|
{},
|
||||||
|
DEFAULT_CONFIG.preview,
|
||||||
|
originalConfig.preview,
|
||||||
|
rcConfig.preview
|
||||||
|
)
|
||||||
|
|
||||||
rewriteHotkey(config)
|
rewriteHotkey(config)
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function addStorage (input) {
|
|||||||
rawStorages = []
|
rawStorages = []
|
||||||
}
|
}
|
||||||
let key = keygen()
|
let key = keygen()
|
||||||
while (rawStorages.some((storage) => storage.key === key)) {
|
while (rawStorages.some(storage => storage.key === key)) {
|
||||||
key = keygen()
|
key = keygen()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +57,10 @@ function addStorage (input) {
|
|||||||
return newStorage
|
return newStorage
|
||||||
})
|
})
|
||||||
.then(function(storage) {
|
.then(function(storage) {
|
||||||
return resolveStorageNotes(storage)
|
return resolveStorageNotes(storage).then(notes => {
|
||||||
.then((notes) => {
|
|
||||||
let unknownCount = 0
|
let unknownCount = 0
|
||||||
notes.forEach((note) => {
|
notes.forEach(note => {
|
||||||
if (!storage.folders.some((folder) => note.folder === folder.key)) {
|
if (!storage.folders.some(folder => note.folder === folder.key)) {
|
||||||
unknownCount++
|
unknownCount++
|
||||||
storage.folders.push({
|
storage.folders.push({
|
||||||
key: note.folder,
|
key: note.folder,
|
||||||
@@ -71,7 +70,10 @@ function addStorage (input) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (unknownCount > 0) {
|
if (unknownCount > 0) {
|
||||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'boostnote.json'),
|
||||||
|
_.pick(storage, ['folders', 'version'])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return notes
|
return notes
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { isString } from 'lodash'
|
|||||||
|
|
||||||
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
||||||
const DESTINATION_FOLDER = 'attachments'
|
const DESTINATION_FOLDER = 'attachments'
|
||||||
const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
|
const PATH_SEPARATORS =
|
||||||
|
escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
|
||||||
/**
|
/**
|
||||||
* @description
|
* @description
|
||||||
* Create a Image element to get the real size of image.
|
* Create a Image element to get the real size of image.
|
||||||
@@ -60,33 +61,34 @@ function getOrientation (file) {
|
|||||||
const view = new DataView(arrayBuffer)
|
const view = new DataView(arrayBuffer)
|
||||||
|
|
||||||
// Not start with SOI(Start of image) Marker return fail value
|
// Not start with SOI(Start of image) Marker return fail value
|
||||||
if (view.getUint16(0, false) !== 0xFFD8) return -2
|
if (view.getUint16(0, false) !== 0xffd8) return -2
|
||||||
const length = view.byteLength
|
const length = view.byteLength
|
||||||
let offset = 2
|
let offset = 2
|
||||||
while (offset < length) {
|
while (offset < length) {
|
||||||
const marker = view.getUint16(offset, false)
|
const marker = view.getUint16(offset, false)
|
||||||
offset += 2
|
offset += 2
|
||||||
// Loop and seed for APP1 Marker
|
// Loop and seed for APP1 Marker
|
||||||
if (marker === 0xFFE1) {
|
if (marker === 0xffe1) {
|
||||||
// return fail value if it isn't EXIF data
|
// return fail value if it isn't EXIF data
|
||||||
if (view.getUint32(offset += 2, false) !== 0x45786966) {
|
if (view.getUint32((offset += 2), false) !== 0x45786966) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
// Read TIFF header,
|
// Read TIFF header,
|
||||||
// First 2bytes defines byte align of TIFF data.
|
// First 2bytes defines byte align of TIFF data.
|
||||||
// If it is 0x4949="II", it means "Intel" type byte align.
|
// If it is 0x4949="II", it means "Intel" type byte align.
|
||||||
// If it is 0x4d4d="MM", it means "Motorola" type byte align
|
// If it is 0x4d4d="MM", it means "Motorola" type byte align
|
||||||
const little = view.getUint16(offset += 6, false) === 0x4949
|
const little = view.getUint16((offset += 6), false) === 0x4949
|
||||||
offset += view.getUint32(offset + 4, little)
|
offset += view.getUint32(offset + 4, little)
|
||||||
const tags = view.getUint16(offset, little) // Get TAG number
|
const tags = view.getUint16(offset, little) // Get TAG number
|
||||||
offset += 2
|
offset += 2
|
||||||
for (let i = 0; i < tags; i++) {
|
for (let i = 0; i < tags; i++) {
|
||||||
// Loop to find Orientation TAG and return the value
|
// Loop to find Orientation TAG and return the value
|
||||||
if (view.getUint16(offset + (i * 12), little) === 0x0112) {
|
if (view.getUint16(offset + i * 12, little) === 0x0112) {
|
||||||
return view.getUint16(offset + (i * 12) + 8, little)
|
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
|
break
|
||||||
} else {
|
} else {
|
||||||
offset += view.getUint16(offset, false)
|
offset += view.getUint16(offset, false)
|
||||||
@@ -94,7 +96,7 @@ function getOrientation (file) {
|
|||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
return new Promise(resolve => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = event => resolve(getData(event.target.result))
|
reader.onload = event => resolve(getData(event.target.result))
|
||||||
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
|
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
|
||||||
@@ -108,8 +110,8 @@ function getOrientation (file) {
|
|||||||
* @return {String} Base64 encoded image.
|
* @return {String} Base64 encoded image.
|
||||||
*/
|
*/
|
||||||
function fixRotate(file) {
|
function fixRotate(file) {
|
||||||
return Promise.all([getImage(file), getOrientation(file)])
|
return Promise.all([getImage(file), getOrientation(file)]).then(
|
||||||
.then(([img, orientation]) => {
|
([img, orientation]) => {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
if (orientation > 4 && orientation < 9) {
|
if (orientation > 4 && orientation < 9) {
|
||||||
@@ -120,18 +122,34 @@ function fixRotate (file) {
|
|||||||
canvas.height = img.height
|
canvas.height = img.height
|
||||||
}
|
}
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break
|
case 2:
|
||||||
case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break
|
ctx.transform(-1, 0, 0, 1, img.width, 0)
|
||||||
case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break
|
break
|
||||||
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break
|
case 3:
|
||||||
case 6: ctx.transform(0, 1, -1, 0, img.height, 0); break
|
ctx.transform(-1, 0, 0, -1, img.width, img.height)
|
||||||
case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break
|
break
|
||||||
case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break
|
case 4:
|
||||||
default: break
|
ctx.transform(1, 0, 0, -1, 0, img.height)
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
ctx.transform(0, 1, 1, 0, 0, 0)
|
||||||
|
break
|
||||||
|
case 6:
|
||||||
|
ctx.transform(0, 1, -1, 0, img.height, 0)
|
||||||
|
break
|
||||||
|
case 7:
|
||||||
|
ctx.transform(0, -1, -1, 0, img.height, img.width)
|
||||||
|
break
|
||||||
|
case 8:
|
||||||
|
ctx.transform(0, -1, 1, 0, 0, img.width)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
ctx.drawImage(img, 0, 0)
|
ctx.drawImage(img, 0, 0)
|
||||||
return canvas.toDataURL()
|
return canvas.toDataURL()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,7 +163,12 @@ function fixRotate (file) {
|
|||||||
* @param {boolean} useRandomName determines whether a random filename for the new file is used. If false the source file name is used
|
* @param {boolean} useRandomName determines whether a random filename for the new file is used. If false the source file name is used
|
||||||
* @return {Promise<String>} name (inclusive extension) of the generated file
|
* @return {Promise<String>} name (inclusive extension) of the generated file
|
||||||
*/
|
*/
|
||||||
function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = true) {
|
function copyAttachment(
|
||||||
|
sourceFilePath,
|
||||||
|
storageKey,
|
||||||
|
noteKey,
|
||||||
|
useRandomName = true
|
||||||
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!sourceFilePath) {
|
if (!sourceFilePath) {
|
||||||
reject('sourceFilePath has to be given')
|
reject('sourceFilePath has to be given')
|
||||||
@@ -160,28 +183,41 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
|
const isBase64 =
|
||||||
|
typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
|
||||||
if (!isBase64 && !fs.existsSync(sourceFilePath)) {
|
if (!isBase64 && !fs.existsSync(sourceFilePath)) {
|
||||||
return reject('source file does not exist')
|
return reject('source file does not exist')
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourcePath = sourceFilePath.sourceFilePath || sourceFilePath
|
const sourcePath = sourceFilePath.sourceFilePath || sourceFilePath
|
||||||
const sourceURL = url.parse(/^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath)
|
const sourceURL = url.parse(
|
||||||
|
/^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath
|
||||||
|
)
|
||||||
|
|
||||||
let destinationName
|
let destinationName
|
||||||
if (useRandomName) {
|
if (useRandomName) {
|
||||||
destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) || '.png'}`
|
destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) ||
|
||||||
|
'.png'}`
|
||||||
} else {
|
} else {
|
||||||
destinationName = path.basename(sourceURL.pathname)
|
destinationName = path.basename(sourceURL.pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetStorage = findStorage.findStorage(storageKey)
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
const destinationDir = path.join(
|
||||||
|
targetStorage.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
noteKey
|
||||||
|
)
|
||||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
|
const outputFile = fs.createWriteStream(
|
||||||
|
path.join(destinationDir, destinationName)
|
||||||
|
)
|
||||||
|
|
||||||
if (isBase64) {
|
if (isBase64) {
|
||||||
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
|
const base64Data = sourceFilePath.data.replace(
|
||||||
|
/^data:image\/\w+;base64,/,
|
||||||
|
''
|
||||||
|
)
|
||||||
const dataBuffer = Buffer.from(base64Data, 'base64')
|
const dataBuffer = Buffer.from(base64Data, 'base64')
|
||||||
outputFile.write(dataBuffer, () => {
|
outputFile.write(dataBuffer, () => {
|
||||||
resolve(destinationName)
|
resolve(destinationName)
|
||||||
@@ -204,7 +240,11 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
|
|||||||
if (!fs.existsSync(destinationDir)) {
|
if (!fs.existsSync(destinationDir)) {
|
||||||
fs.mkdirSync(destinationDir)
|
fs.mkdirSync(destinationDir)
|
||||||
}
|
}
|
||||||
destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER, noteKey)
|
destinationDir = path.join(
|
||||||
|
destinationStoragePath,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
noteKey
|
||||||
|
)
|
||||||
if (!fs.existsSync(destinationDir)) {
|
if (!fs.existsSync(destinationDir)) {
|
||||||
fs.mkdirSync(destinationDir)
|
fs.mkdirSync(destinationDir)
|
||||||
}
|
}
|
||||||
@@ -217,16 +257,27 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
|
|||||||
* @param noteKey Key of the current note
|
* @param noteKey Key of the current note
|
||||||
*/
|
*/
|
||||||
function migrateAttachments(markdownContent, storagePath, noteKey) {
|
function migrateAttachments(markdownContent, storagePath, noteKey) {
|
||||||
if (noteKey !== undefined && sander.existsSync(path.join(storagePath, 'images'))) {
|
if (
|
||||||
|
noteKey !== undefined &&
|
||||||
|
sander.existsSync(path.join(storagePath, 'images'))
|
||||||
|
) {
|
||||||
const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
|
const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
|
||||||
if (attachments.length) {
|
if (attachments.length) {
|
||||||
createAttachmentDestinationFolder(storagePath, noteKey)
|
createAttachmentDestinationFolder(storagePath, noteKey)
|
||||||
}
|
}
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
const attachmentBaseName = path.basename(attachment)
|
const attachmentBaseName = path.basename(attachment)
|
||||||
const possibleLegacyPath = path.join(storagePath, 'images', attachmentBaseName)
|
const possibleLegacyPath = path.join(
|
||||||
|
storagePath,
|
||||||
|
'images',
|
||||||
|
attachmentBaseName
|
||||||
|
)
|
||||||
if (sander.existsSync(possibleLegacyPath)) {
|
if (sander.existsSync(possibleLegacyPath)) {
|
||||||
const destinationPath = path.join(storagePath, DESTINATION_FOLDER, attachmentBaseName)
|
const destinationPath = path.join(
|
||||||
|
storagePath,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
attachmentBaseName
|
||||||
|
)
|
||||||
if (!sander.existsSync(destinationPath)) {
|
if (!sander.existsSync(destinationPath)) {
|
||||||
sander.copyFileSync(possibleLegacyPath).to(destinationPath)
|
sander.copyFileSync(possibleLegacyPath).to(destinationPath)
|
||||||
}
|
}
|
||||||
@@ -244,7 +295,8 @@ function migrateAttachments (markdownContent, storagePath, noteKey) {
|
|||||||
function fixLocalURLS(renderedHTML, storagePath) {
|
function fixLocalURLS(renderedHTML, storagePath) {
|
||||||
const encodedWin32SeparatorRegex = /%5C/g
|
const encodedWin32SeparatorRegex = /%5C/g
|
||||||
const storageRegex = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g')
|
const storageRegex = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g')
|
||||||
const storageUrl = 'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/')
|
const storageUrl =
|
||||||
|
'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
|
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
|
||||||
@@ -254,9 +306,17 @@ function fixLocalURLS (renderedHTML, storagePath) {
|
|||||||
- `(?:\\\/|%5C)[-.\\w]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg`
|
- `(?:\\\/|%5C)[-.\\w]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg`
|
||||||
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
|
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
|
||||||
*/
|
*/
|
||||||
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
|
return renderedHTML.replace(
|
||||||
return match.replace(encodedWin32SeparatorRegex, '/').replace(storageRegex, storageUrl)
|
new RegExp(
|
||||||
})
|
'/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\/|%5C)[-.\\w]+)+',
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
function(match) {
|
||||||
|
return match
|
||||||
|
.replace(encodedWin32SeparatorRegex, '/')
|
||||||
|
.replace(storageRegex, storageUrl)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -281,50 +341,63 @@ function generateAttachmentMarkdown (fileName, path, showPreview) {
|
|||||||
function handleAttachmentDrop(codeEditor, storageKey, noteKey, dropEvent) {
|
function handleAttachmentDrop(codeEditor, storageKey, noteKey, dropEvent) {
|
||||||
let promise
|
let promise
|
||||||
if (dropEvent.dataTransfer.files.length > 0) {
|
if (dropEvent.dataTransfer.files.length > 0) {
|
||||||
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
|
promise = Promise.all(
|
||||||
|
Array.from(dropEvent.dataTransfer.files).map(file => {
|
||||||
const filePath = file.path
|
const filePath = file.path
|
||||||
const fileType = file.type // EX) 'image/gif' or 'text/html'
|
const fileType = file.type // EX) 'image/gif' or 'text/html'
|
||||||
if (fileType.startsWith('image')) {
|
if (fileType.startsWith('image')) {
|
||||||
if (fileType === 'image/gif' || fileType === 'image/svg+xml') {
|
if (fileType === 'image/gif' || fileType === 'image/svg+xml') {
|
||||||
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
|
return copyAttachment(filePath, storageKey, noteKey).then(
|
||||||
fileName,
|
fileName => ({
|
||||||
title: path.basename(filePath),
|
|
||||||
isImage: true
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
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,
|
fileName,
|
||||||
title: path.basename(filePath),
|
title: path.basename(filePath),
|
||||||
isImage: true
|
isImage: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
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(filePath),
|
||||||
|
isImage: true
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
|
return copyAttachment(filePath, storageKey, noteKey).then(
|
||||||
|
fileName => ({
|
||||||
fileName,
|
fileName,
|
||||||
title: path.basename(filePath),
|
title: path.basename(filePath),
|
||||||
isImage: false
|
isImage: false
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
let imageURL = dropEvent.dataTransfer.getData('text/plain')
|
let imageURL = dropEvent.dataTransfer.getData('text/plain')
|
||||||
|
|
||||||
if (!imageURL) {
|
if (!imageURL) {
|
||||||
const match = /<img[^>]*[\s"']src="([^"]+)"/.exec(dropEvent.dataTransfer.getData('text/html'))
|
const match = /<img[^>]*[\s"']src="([^"]+)"/.exec(
|
||||||
|
dropEvent.dataTransfer.getData('text/html')
|
||||||
|
)
|
||||||
if (match) {
|
if (match) {
|
||||||
imageURL = match[1]
|
imageURL = match[1]
|
||||||
}
|
}
|
||||||
@@ -334,7 +407,8 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
promise = Promise.all([getImage(imageURL)
|
promise = Promise.all([
|
||||||
|
getImage(imageURL)
|
||||||
.then(image => {
|
.then(image => {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
@@ -342,11 +416,15 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
|||||||
canvas.height = image.height
|
canvas.height = image.height
|
||||||
context.drawImage(image, 0, 0)
|
context.drawImage(image, 0, 0)
|
||||||
|
|
||||||
return copyAttachment({
|
return copyAttachment(
|
||||||
|
{
|
||||||
type: 'base64',
|
type: 'base64',
|
||||||
data: canvas.toDataURL(),
|
data: canvas.toDataURL(),
|
||||||
sourceFilePath: imageURL
|
sourceFilePath: imageURL
|
||||||
}, storageKey, noteKey)
|
},
|
||||||
|
storageKey,
|
||||||
|
noteKey
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.then(fileName => ({
|
.then(fileName => ({
|
||||||
fileName,
|
fileName,
|
||||||
@@ -357,7 +435,15 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
promise.then(files => {
|
promise.then(files => {
|
||||||
const attachments = files.filter(file => !!file).map(file => generateAttachmentMarkdown(file.title, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, file.fileName), file.isImage))
|
const attachments = files
|
||||||
|
.filter(file => !!file)
|
||||||
|
.map(file =>
|
||||||
|
generateAttachmentMarkdown(
|
||||||
|
file.title,
|
||||||
|
path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, file.fileName),
|
||||||
|
file.isImage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
codeEditor.insertAttachmentMd(attachments.join('\n'))
|
codeEditor.insertAttachmentMd(attachments.join('\n'))
|
||||||
})
|
})
|
||||||
@@ -370,7 +456,12 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
|||||||
* @param {String} noteKey Key of the current note
|
* @param {String} noteKey Key of the current note
|
||||||
* @param {DataTransferItem} dataTransferItem Part of the past-event
|
* @param {DataTransferItem} dataTransferItem Part of the past-event
|
||||||
*/
|
*/
|
||||||
function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
|
function handlePasteImageEvent(
|
||||||
|
codeEditor,
|
||||||
|
storageKey,
|
||||||
|
noteKey,
|
||||||
|
dataTransferItem
|
||||||
|
) {
|
||||||
if (!codeEditor) {
|
if (!codeEditor) {
|
||||||
throw new Error('codeEditor has to be given')
|
throw new Error('codeEditor has to be given')
|
||||||
}
|
}
|
||||||
@@ -389,7 +480,11 @@ function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferIte
|
|||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
let base64data
|
let base64data
|
||||||
const targetStorage = findStorage.findStorage(storageKey)
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
const destinationDir = path.join(
|
||||||
|
targetStorage.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
noteKey
|
||||||
|
)
|
||||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
|
|
||||||
const imageName = `${uniqueSlug()}.png`
|
const imageName = `${uniqueSlug()}.png`
|
||||||
@@ -400,8 +495,16 @@ function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferIte
|
|||||||
base64data += base64data.replace('+', ' ')
|
base64data += base64data.replace('+', ' ')
|
||||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||||
fs.writeFileSync(imagePath, binaryData, 'binary')
|
fs.writeFileSync(imagePath, binaryData, 'binary')
|
||||||
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
|
const imageReferencePath = path.join(
|
||||||
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
|
STORAGE_FOLDER_PLACEHOLDER,
|
||||||
|
noteKey,
|
||||||
|
imageName
|
||||||
|
)
|
||||||
|
const imageMd = generateAttachmentMarkdown(
|
||||||
|
imageName,
|
||||||
|
imageReferencePath,
|
||||||
|
true
|
||||||
|
)
|
||||||
codeEditor.insertAttachmentMd(imageMd)
|
codeEditor.insertAttachmentMd(imageMd)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(blob)
|
reader.readAsDataURL(blob)
|
||||||
@@ -430,7 +533,11 @@ function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetStorage = findStorage.findStorage(storageKey)
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
const destinationDir = path.join(
|
||||||
|
targetStorage.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
noteKey
|
||||||
|
)
|
||||||
|
|
||||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
|
|
||||||
@@ -440,8 +547,16 @@ function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
|
|||||||
const binaryData = image.toPNG()
|
const binaryData = image.toPNG()
|
||||||
fs.writeFileSync(imagePath, binaryData, 'binary')
|
fs.writeFileSync(imagePath, binaryData, 'binary')
|
||||||
|
|
||||||
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
|
const imageReferencePath = path.join(
|
||||||
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
|
STORAGE_FOLDER_PLACEHOLDER,
|
||||||
|
noteKey,
|
||||||
|
imageName
|
||||||
|
)
|
||||||
|
const imageMd = generateAttachmentMarkdown(
|
||||||
|
imageName,
|
||||||
|
imageReferencePath,
|
||||||
|
true
|
||||||
|
)
|
||||||
codeEditor.insertAttachmentMd(imageMd)
|
codeEditor.insertAttachmentMd(imageMd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,8 +566,23 @@ function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
|
|||||||
* @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
|
* @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
|
||||||
*/
|
*/
|
||||||
function getAttachmentsInMarkdownContent(markdownContent) {
|
function getAttachmentsInMarkdownContent(markdownContent) {
|
||||||
const preparedInput = markdownContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
|
const preparedInput = markdownContent.replace(
|
||||||
const regexp = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + ')' + '?([a-zA-Z0-9]|-)*' + '(' + escapeStringRegexp(path.sep) + ')' + '([a-zA-Z0-9]|\\.)+(\\.[a-zA-Z0-9]+)?', 'g')
|
new RegExp('[' + PATH_SEPARATORS + ']', 'g'),
|
||||||
|
path.sep
|
||||||
|
)
|
||||||
|
const regexp = new RegExp(
|
||||||
|
'/?' +
|
||||||
|
STORAGE_FOLDER_PLACEHOLDER +
|
||||||
|
'(' +
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
')' +
|
||||||
|
'?([a-zA-Z0-9]|-)*' +
|
||||||
|
'(' +
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
')' +
|
||||||
|
'([a-zA-Z0-9]|\\.)+(\\.[a-zA-Z0-9]+)?',
|
||||||
|
'g'
|
||||||
|
)
|
||||||
return preparedInput.match(regexp)
|
return preparedInput.match(regexp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +596,12 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
|
|||||||
const temp = getAttachmentsInMarkdownContent(markdownContent) || []
|
const temp = getAttachmentsInMarkdownContent(markdownContent) || []
|
||||||
const result = []
|
const result = []
|
||||||
for (const relativePath of temp) {
|
for (const relativePath of temp) {
|
||||||
result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER)))
|
result.push(
|
||||||
|
relativePath.replace(
|
||||||
|
new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'),
|
||||||
|
path.join(storagePath, DESTINATION_FOLDER)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -489,8 +624,12 @@ function importAttachments (markDownContent, filepath, storageKey, noteKey) {
|
|||||||
while (attachPath) {
|
while (attachPath) {
|
||||||
let attachmentPath = attachPath[groupIndex]
|
let attachmentPath = attachPath[groupIndex]
|
||||||
attachmentPaths.push(attachmentPath)
|
attachmentPaths.push(attachmentPath)
|
||||||
attachmentPath = path.isAbsolute(attachmentPath) ? attachmentPath : path.join(path.dirname(filepath), attachmentPath)
|
attachmentPath = path.isAbsolute(attachmentPath)
|
||||||
promiseArray.push(this.copyAttachment(attachmentPath, storageKey, noteKey))
|
? attachmentPath
|
||||||
|
: path.join(path.dirname(filepath), attachmentPath)
|
||||||
|
promiseArray.push(
|
||||||
|
this.copyAttachment(attachmentPath, storageKey, noteKey)
|
||||||
|
)
|
||||||
attachPath = nameRegex.exec(markDownContent)
|
attachPath = nameRegex.exec(markDownContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,11 +641,15 @@ function importAttachments (markDownContent, filepath, storageKey, noteKey) {
|
|||||||
|
|
||||||
for (let j = 0; j < promiseArray.length; j++) {
|
for (let j = 0; j < promiseArray.length; j++) {
|
||||||
promiseArray[j]
|
promiseArray[j]
|
||||||
.then((fileName) => {
|
.then(fileName => {
|
||||||
const newPath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName)
|
const newPath = path.join(
|
||||||
|
STORAGE_FOLDER_PLACEHOLDER,
|
||||||
|
noteKey,
|
||||||
|
fileName
|
||||||
|
)
|
||||||
markDownContent = markDownContent.replace(attachmentPaths[j], newPath)
|
markDownContent = markDownContent.replace(attachmentPaths[j], newPath)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch(e => {
|
||||||
console.error('File does not exist in path: ' + attachmentPaths[j])
|
console.error('File does not exist in path: ' + attachmentPaths[j])
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -547,8 +690,17 @@ function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) {
|
|||||||
*/
|
*/
|
||||||
function replaceNoteKeyWithNewNoteKey(noteContent, oldNoteKey, newNoteKey) {
|
function replaceNoteKeyWithNewNoteKey(noteContent, oldNoteKey, newNoteKey) {
|
||||||
if (noteContent) {
|
if (noteContent) {
|
||||||
const preparedInput = noteContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
|
const preparedInput = noteContent.replace(
|
||||||
return preparedInput.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
|
new RegExp('[' + PATH_SEPARATORS + ']', 'g'),
|
||||||
|
path.sep
|
||||||
|
)
|
||||||
|
return preparedInput.replace(
|
||||||
|
new RegExp(
|
||||||
|
STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey,
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return noteContent
|
return noteContent
|
||||||
}
|
}
|
||||||
@@ -560,14 +712,27 @@ function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
|
|||||||
* @returns {String} Input without the references
|
* @returns {String} Input without the references
|
||||||
*/
|
*/
|
||||||
function removeStorageAndNoteReferences(input, noteKey) {
|
function removeStorageAndNoteReferences(input, noteKey) {
|
||||||
return input.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'), function (match) {
|
return input.replace(
|
||||||
|
new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'),
|
||||||
|
function(match) {
|
||||||
const temp = match
|
const temp = match
|
||||||
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep)
|
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep)
|
||||||
.replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep)
|
.replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep)
|
||||||
.replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep)
|
.replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep)
|
||||||
.replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep)
|
.replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep)
|
||||||
return temp.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
|
return temp.replace(
|
||||||
})
|
new RegExp(
|
||||||
|
STORAGE_FOLDER_PLACEHOLDER +
|
||||||
|
'(' +
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
noteKey +
|
||||||
|
')?',
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
DESTINATION_FOLDER
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -577,7 +742,11 @@ function removeStorageAndNoteReferences (input, noteKey) {
|
|||||||
*/
|
*/
|
||||||
function deleteAttachmentFolder(storageKey, noteKey) {
|
function deleteAttachmentFolder(storageKey, noteKey) {
|
||||||
const storagePath = findStorage.findStorage(storageKey)
|
const storagePath = findStorage.findStorage(storageKey)
|
||||||
const noteAttachmentPath = path.join(storagePath.path, DESTINATION_FOLDER, noteKey)
|
const noteAttachmentPath = path.join(
|
||||||
|
storagePath.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
noteKey
|
||||||
|
)
|
||||||
sander.rimrafSync(noteAttachmentPath)
|
sander.rimrafSync(noteAttachmentPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,36 +756,66 @@ function deleteAttachmentFolder (storageKey, noteKey) {
|
|||||||
* @param storageKey StorageKey of the current note. Is used to determine the belonging attachment folder.
|
* @param storageKey StorageKey of the current note. Is used to determine the belonging attachment folder.
|
||||||
* @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder.
|
* @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder.
|
||||||
*/
|
*/
|
||||||
function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey) {
|
function deleteAttachmentsNotPresentInNote(
|
||||||
|
markdownContent,
|
||||||
|
storageKey,
|
||||||
|
noteKey
|
||||||
|
) {
|
||||||
if (storageKey == null || noteKey == null || markdownContent == null) {
|
if (storageKey == null || noteKey == null || markdownContent == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const targetStorage = findStorage.findStorage(storageKey)
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
const attachmentFolder = path.join(
|
||||||
|
targetStorage.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
noteKey
|
||||||
|
)
|
||||||
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
|
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
|
||||||
const attachmentsInNoteOnlyFileNames = []
|
const attachmentsInNoteOnlyFileNames = []
|
||||||
if (attachmentsInNote) {
|
if (attachmentsInNote) {
|
||||||
for (let i = 0; i < attachmentsInNote.length; i++) {
|
for (let i = 0; i < attachmentsInNote.length; i++) {
|
||||||
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
|
attachmentsInNoteOnlyFileNames.push(
|
||||||
|
attachmentsInNote[i].replace(
|
||||||
|
new RegExp(
|
||||||
|
STORAGE_FOLDER_PLACEHOLDER +
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
noteKey +
|
||||||
|
escapeStringRegexp(path.sep),
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fs.existsSync(attachmentFolder)) {
|
if (fs.existsSync(attachmentFolder)) {
|
||||||
fs.readdir(attachmentFolder, (err, files) => {
|
fs.readdir(attachmentFolder, (err, files) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error reading directory "' + attachmentFolder + '". Error:')
|
console.error(
|
||||||
|
'Error reading directory "' + attachmentFolder + '". Error:'
|
||||||
|
)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
|
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
|
||||||
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
|
const absolutePathOfFile = path.join(
|
||||||
fs.unlink(absolutePathOfFile, (err) => {
|
targetStorage.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
noteKey,
|
||||||
|
file
|
||||||
|
)
|
||||||
|
fs.unlink(absolutePathOfFile, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Could not delete "%s"', absolutePathOfFile)
|
console.error('Could not delete "%s"', absolutePathOfFile)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.info('File "' + absolutePathOfFile + '" deleted because it was not included in the content of the note')
|
console.info(
|
||||||
|
'File "' +
|
||||||
|
absolutePathOfFile +
|
||||||
|
'" deleted because it was not included in the content of the note'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -637,26 +836,48 @@ function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const targetStorage = findStorage.findStorage(storageKey)
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
const attachmentFolder = path.join(
|
||||||
|
targetStorage.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
noteKey
|
||||||
|
)
|
||||||
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
|
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
|
||||||
const attachmentsInNoteOnlyFileNames = []
|
const attachmentsInNoteOnlyFileNames = []
|
||||||
if (attachmentsInNote) {
|
if (attachmentsInNote) {
|
||||||
for (let i = 0; i < attachmentsInNote.length; i++) {
|
for (let i = 0; i < attachmentsInNote.length; i++) {
|
||||||
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
|
attachmentsInNoteOnlyFileNames.push(
|
||||||
|
attachmentsInNote[i].replace(
|
||||||
|
new RegExp(
|
||||||
|
STORAGE_FOLDER_PLACEHOLDER +
|
||||||
|
escapeStringRegexp(path.sep) +
|
||||||
|
noteKey +
|
||||||
|
escapeStringRegexp(path.sep),
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fs.existsSync(attachmentFolder)) {
|
if (fs.existsSync(attachmentFolder)) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.readdir(attachmentFolder, (err, files) => {
|
fs.readdir(attachmentFolder, (err, files) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error reading directory "' + attachmentFolder + '". Error:')
|
console.error(
|
||||||
|
'Error reading directory "' + attachmentFolder + '". Error:'
|
||||||
|
)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
reject(err)
|
reject(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const attachments = []
|
const attachments = []
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
|
const absolutePathOfFile = path.join(
|
||||||
|
targetStorage.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
noteKey,
|
||||||
|
file
|
||||||
|
)
|
||||||
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
|
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
|
||||||
attachments.push({ path: absolutePathOfFile, isInUse: false })
|
attachments.push({ path: absolutePathOfFile, isInUse: false })
|
||||||
} else {
|
} else {
|
||||||
@@ -679,7 +900,7 @@ function removeAttachmentsByPaths (attachments) {
|
|||||||
const promises = []
|
const promises = []
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
fs.unlink(attachment, (err) => {
|
fs.unlink(attachment, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Could not delete "%s"', attachment)
|
console.error('Could not delete "%s"', attachment)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@@ -704,25 +925,50 @@ function cloneAttachments (oldNote, newNote) {
|
|||||||
if (newNote.type === 'MARKDOWN_NOTE') {
|
if (newNote.type === 'MARKDOWN_NOTE') {
|
||||||
const oldStorage = findStorage.findStorage(oldNote.storage)
|
const oldStorage = findStorage.findStorage(oldNote.storage)
|
||||||
const newStorage = findStorage.findStorage(newNote.storage)
|
const newStorage = findStorage.findStorage(newNote.storage)
|
||||||
const attachmentsPaths = getAbsolutePathsOfAttachmentsInContent(oldNote.content, oldStorage.path) || []
|
const attachmentsPaths =
|
||||||
|
getAbsolutePathsOfAttachmentsInContent(
|
||||||
|
oldNote.content,
|
||||||
|
oldStorage.path
|
||||||
|
) || []
|
||||||
|
|
||||||
const destinationFolder = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key)
|
const destinationFolder = path.join(
|
||||||
|
newStorage.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
newNote.key
|
||||||
|
)
|
||||||
if (!sander.existsSync(destinationFolder)) {
|
if (!sander.existsSync(destinationFolder)) {
|
||||||
sander.mkdirSync(destinationFolder)
|
sander.mkdirSync(destinationFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const attachment of attachmentsPaths) {
|
for (const attachment of attachmentsPaths) {
|
||||||
const destination = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key, path.basename(attachment))
|
const destination = path.join(
|
||||||
|
newStorage.path,
|
||||||
|
DESTINATION_FOLDER,
|
||||||
|
newNote.key,
|
||||||
|
path.basename(attachment)
|
||||||
|
)
|
||||||
sander.copyFileSync(attachment).to(destination)
|
sander.copyFileSync(attachment).to(destination)
|
||||||
}
|
}
|
||||||
newNote.content = replaceNoteKeyWithNewNoteKey(newNote.content, oldNote.key, newNote.key)
|
newNote.content = replaceNoteKeyWithNewNoteKey(
|
||||||
|
newNote.content,
|
||||||
|
oldNote.key,
|
||||||
|
newNote.key
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
console.debug('Cloning of the attachment was skipped since it only works for MARKDOWN_NOTEs')
|
console.debug(
|
||||||
|
'Cloning of the attachment was skipped since it only works for MARKDOWN_NOTEs'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateFileNotFoundMarkdown() {
|
function generateFileNotFoundMarkdown() {
|
||||||
return '**' + i18n.__('⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠') + '**'
|
return (
|
||||||
|
'**' +
|
||||||
|
i18n.__(
|
||||||
|
'⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠'
|
||||||
|
) +
|
||||||
|
'**'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -732,7 +978,19 @@ function generateFileNotFoundMarkdown () {
|
|||||||
*/
|
*/
|
||||||
function isAttachmentLink(text) {
|
function isAttachmentLink(text) {
|
||||||
if (text) {
|
if (text) {
|
||||||
return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + '[' + PATH_SEPARATORS + ']' + '.*\\).*', 'gi')) != null
|
return (
|
||||||
|
text.match(
|
||||||
|
new RegExp(
|
||||||
|
'.*\\[.*\\]\\( *' +
|
||||||
|
escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) +
|
||||||
|
'[' +
|
||||||
|
PATH_SEPARATORS +
|
||||||
|
']' +
|
||||||
|
'.*\\).*',
|
||||||
|
'gi'
|
||||||
|
)
|
||||||
|
) != null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -752,21 +1010,52 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
|
|||||||
const replaceInstructions = []
|
const replaceInstructions = []
|
||||||
const copies = []
|
const copies = []
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
const absPathOfAttachment = attachment.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER))
|
const absPathOfAttachment = attachment.replace(
|
||||||
|
new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'),
|
||||||
|
path.join(storagePath, DESTINATION_FOLDER)
|
||||||
|
)
|
||||||
copies.push(
|
copies.push(
|
||||||
sander.exists(absPathOfAttachment)
|
sander.exists(absPathOfAttachment).then(fileExists => {
|
||||||
.then((fileExists) => {
|
|
||||||
if (!fileExists) {
|
if (!fileExists) {
|
||||||
const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')'))
|
const fileNotFoundRegexp = new RegExp(
|
||||||
replaceInstructions.push({regexp: fileNotFoundRegexp, replacement: this.generateFileNotFoundMarkdown()})
|
'!?' +
|
||||||
|
escapeStringRegexp('[') +
|
||||||
|
'[\\w|\\d|\\s|\\.]*\\]\\(\\s*' +
|
||||||
|
STORAGE_FOLDER_PLACEHOLDER +
|
||||||
|
'[\\w|\\d|\\-|' +
|
||||||
|
PATH_SEPARATORS +
|
||||||
|
']*' +
|
||||||
|
escapeStringRegexp(path.basename(absPathOfAttachment)) +
|
||||||
|
escapeStringRegexp(')')
|
||||||
|
)
|
||||||
|
replaceInstructions.push({
|
||||||
|
regexp: fileNotFoundRegexp,
|
||||||
|
replacement: this.generateFileNotFoundMarkdown()
|
||||||
|
})
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
return this.copyAttachment(absPathOfAttachment, storageKey, noteKey)
|
return this.copyAttachment(
|
||||||
.then((fileName) => {
|
absPathOfAttachment,
|
||||||
const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')'))
|
storageKey,
|
||||||
|
noteKey
|
||||||
|
).then(fileName => {
|
||||||
|
const replaceLinkRegExp = new RegExp(
|
||||||
|
escapeStringRegexp('(') +
|
||||||
|
' *' +
|
||||||
|
STORAGE_FOLDER_PLACEHOLDER +
|
||||||
|
'[\\w|\\d|\\-|' +
|
||||||
|
PATH_SEPARATORS +
|
||||||
|
']*' +
|
||||||
|
escapeStringRegexp(path.basename(absPathOfAttachment)) +
|
||||||
|
' *' +
|
||||||
|
escapeStringRegexp(')')
|
||||||
|
)
|
||||||
replaceInstructions.push({
|
replaceInstructions.push({
|
||||||
regexp: replaceLinkRegExp,
|
regexp: replaceLinkRegExp,
|
||||||
replacement: '(' + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + ')'
|
replacement:
|
||||||
|
'(' +
|
||||||
|
path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) +
|
||||||
|
')'
|
||||||
})
|
})
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
})
|
})
|
||||||
@@ -776,7 +1065,10 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
|
|||||||
return Promise.all(copies).then(() => {
|
return Promise.all(copies).then(() => {
|
||||||
let modifiedLinkText = linkText
|
let modifiedLinkText = linkText
|
||||||
for (const replaceInstruction of replaceInstructions) {
|
for (const replaceInstruction of replaceInstructions) {
|
||||||
modifiedLinkText = modifiedLinkText.replace(replaceInstruction.regexp, replaceInstruction.replacement)
|
modifiedLinkText = modifiedLinkText.replace(
|
||||||
|
replaceInstruction.regexp,
|
||||||
|
replaceInstruction.replacement
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return modifiedLinkText
|
return modifiedLinkText
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,10 +34,9 @@ function createFolder (storageKey, input) {
|
|||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage).then(function createFolder(storage) {
|
||||||
.then(function createFolder (storage) {
|
|
||||||
let key = keygen()
|
let key = keygen()
|
||||||
while (storage.folders.some((folder) => folder.key === key)) {
|
while (storage.folders.some(folder => folder.key === key)) {
|
||||||
key = keygen()
|
key = keygen()
|
||||||
}
|
}
|
||||||
const newFolder = {
|
const newFolder = {
|
||||||
@@ -48,7 +47,10 @@ function createFolder (storageKey, input) {
|
|||||||
|
|
||||||
storage.folders.push(newFolder)
|
storage.folders.push(newFolder)
|
||||||
|
|
||||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'boostnote.json'),
|
||||||
|
_.pick(storage, ['folders', 'version'])
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
storage
|
storage
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ const { findStorage } = require('browser/lib/findStorage')
|
|||||||
|
|
||||||
function validateInput(input) {
|
function validateInput(input) {
|
||||||
if (!_.isArray(input.tags)) input.tags = []
|
if (!_.isArray(input.tags)) input.tags = []
|
||||||
input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0)
|
input.tags = input.tags.filter(
|
||||||
|
tag => _.isString(tag) && tag.trim().length > 0
|
||||||
|
)
|
||||||
if (!_.isString(input.title)) input.title = ''
|
if (!_.isString(input.title)) input.title = ''
|
||||||
input.isStarred = !!input.isStarred
|
input.isStarred = !!input.isStarred
|
||||||
input.isTrashed = !!input.isTrashed
|
input.isTrashed = !!input.isTrashed
|
||||||
@@ -21,16 +23,20 @@ function validateInput (input) {
|
|||||||
case 'SNIPPET_NOTE':
|
case 'SNIPPET_NOTE':
|
||||||
if (!_.isString(input.description)) input.description = ''
|
if (!_.isString(input.description)) input.description = ''
|
||||||
if (!_.isArray(input.snippets)) {
|
if (!_.isArray(input.snippets)) {
|
||||||
input.snippets = [{
|
input.snippets = [
|
||||||
|
{
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'text',
|
mode: 'text',
|
||||||
content: '',
|
content: '',
|
||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
|
throw new Error(
|
||||||
|
'Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +55,7 @@ function createNote (storageKey, input) {
|
|||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage)
|
||||||
.then(function checkFolderExists(storage) {
|
.then(function checkFolderExists(storage) {
|
||||||
if (_.find(storage.folders, { key: input.folder }) == null) {
|
if (_.find(storage.folders, { key: input.folder }) == null) {
|
||||||
throw new Error('Target folder doesn\'t exist.')
|
throw new Error("Target folder doesn't exist.")
|
||||||
}
|
}
|
||||||
return storage
|
return storage
|
||||||
})
|
})
|
||||||
@@ -68,7 +74,8 @@ function createNote (storageKey, input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const noteData = Object.assign({},
|
const noteData = Object.assign(
|
||||||
|
{},
|
||||||
{
|
{
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
@@ -77,9 +84,13 @@ function createNote (storageKey, input) {
|
|||||||
{
|
{
|
||||||
key,
|
key,
|
||||||
storage: storageKey
|
storage: storageKey
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'notes', key + '.cson'),
|
||||||
|
_.omit(noteData, ['key', 'storage'])
|
||||||
|
)
|
||||||
|
|
||||||
return noteData
|
return noteData
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import { push } from 'connected-react-router'
|
|||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
|
||||||
function validateUrl(str) {
|
function validateUrl(str) {
|
||||||
if (/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(str)) {
|
if (
|
||||||
|
/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(
|
||||||
|
str
|
||||||
|
)
|
||||||
|
) {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@@ -15,12 +19,20 @@ function validateUrl (str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ERROR_MESSAGES = {
|
const ERROR_MESSAGES = {
|
||||||
ENOTFOUND: 'URL not found. Please check the URL, or your internet connection and try again.',
|
ENOTFOUND:
|
||||||
VALIDATION_ERROR: 'Please check if the URL follows this format: https://www.google.com',
|
'URL not found. Please check the URL, or your internet connection and try again.',
|
||||||
|
VALIDATION_ERROR:
|
||||||
|
'Please check if the URL follows this format: https://www.google.com',
|
||||||
UNEXPECTED: 'Unexpected error! Please check console for details!'
|
UNEXPECTED: 'Unexpected error! Please check console for details!'
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) {
|
function createNoteFromUrl(
|
||||||
|
url,
|
||||||
|
storage,
|
||||||
|
folder,
|
||||||
|
dispatch = null,
|
||||||
|
location = null
|
||||||
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const td = createTurndownService()
|
const td = createTurndownService()
|
||||||
|
|
||||||
@@ -30,10 +42,10 @@ function createNoteFromUrl (url, storage, folder, dispatch = null, location = nu
|
|||||||
|
|
||||||
const request = url.startsWith('https') ? https : http
|
const request = url.startsWith('https') ? https : http
|
||||||
|
|
||||||
const req = request.request(url, (res) => {
|
const req = request.request(url, res => {
|
||||||
let data = ''
|
let data = ''
|
||||||
|
|
||||||
res.on('data', (chunk) => {
|
res.on('data', chunk => {
|
||||||
data += chunk
|
data += chunk
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -46,17 +58,18 @@ function createNoteFromUrl (url, storage, folder, dispatch = null, location = nu
|
|||||||
folder: folder,
|
folder: folder,
|
||||||
title: '',
|
title: '',
|
||||||
content: markdownHTML
|
content: markdownHTML
|
||||||
})
|
}).then(note => {
|
||||||
.then((note) => {
|
|
||||||
const noteHash = note.key
|
const noteHash = note.key
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
dispatch(push({
|
dispatch(
|
||||||
|
push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: { key: noteHash }
|
query: { key: noteHash }
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
ee.emit('list:jump', noteHash)
|
ee.emit('list:jump', noteHash)
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
resolve({ result: true, error: null })
|
resolve({ result: true, error: null })
|
||||||
@@ -67,16 +80,19 @@ function createNoteFromUrl (url, storage, folder, dispatch = null, location = nu
|
|||||||
folder: folder,
|
folder: folder,
|
||||||
title: '',
|
title: '',
|
||||||
content: markdownHTML
|
content: markdownHTML
|
||||||
}).then((note) => {
|
}).then(note => {
|
||||||
resolve({ result: true, note, error: null })
|
resolve({ result: true, note, error: null })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
req.on('error', (e) => {
|
req.on('error', e => {
|
||||||
console.error('error in parsing URL', e)
|
console.error('error in parsing URL', e)
|
||||||
reject({result: false, error: ERROR_MESSAGES[e.code] || ERROR_MESSAGES.UNEXPECTED})
|
reject({
|
||||||
|
result: false,
|
||||||
|
error: ERROR_MESSAGES[e.code] || ERROR_MESSAGES.UNEXPECTED
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
req.end()
|
req.end()
|
||||||
|
|||||||
@@ -12,13 +12,19 @@ function createSnippet (snippetFile) {
|
|||||||
content: '',
|
content: '',
|
||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
fetchSnippet(null, snippetFile).then((snippets) => {
|
fetchSnippet(null, snippetFile)
|
||||||
|
.then(snippets => {
|
||||||
snippets.push(newSnippet)
|
snippets.push(newSnippet)
|
||||||
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
fs.writeFile(
|
||||||
|
snippetFile || consts.SNIPPET_FILE,
|
||||||
|
JSON.stringify(snippets, null, 4),
|
||||||
|
err => {
|
||||||
if (err) reject(err)
|
if (err) reject(err)
|
||||||
resolve(newSnippet)
|
resolve(newSnippet)
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}).catch((err) => {
|
.catch(err => {
|
||||||
reject(err)
|
reject(err)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ function deleteFolder (storageKey, folderKey) {
|
|||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage)
|
||||||
.then(function assignNotes(storage) {
|
.then(function assignNotes(storage) {
|
||||||
return resolveStorageNotes(storage)
|
return resolveStorageNotes(storage).then(notes => {
|
||||||
.then((notes) => {
|
|
||||||
return {
|
return {
|
||||||
storage,
|
storage,
|
||||||
notes
|
notes
|
||||||
@@ -38,8 +37,9 @@ function deleteFolder (storageKey, folderKey) {
|
|||||||
})
|
})
|
||||||
.then(function deleteFolderAndNotes(data) {
|
.then(function deleteFolderAndNotes(data) {
|
||||||
const { storage, notes } = data
|
const { storage, notes } = data
|
||||||
storage.folders = storage.folders
|
storage.folders = storage.folders.filter(function excludeTargetFolder(
|
||||||
.filter(function excludeTargetFolder (folder) {
|
folder
|
||||||
|
) {
|
||||||
return folder.key !== folderKey
|
return folder.key !== folderKey
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -47,15 +47,16 @@ function deleteFolder (storageKey, folderKey) {
|
|||||||
return note.folder === folderKey
|
return note.folder === folderKey
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteAllNotes = targetNotes
|
const deleteAllNotes = targetNotes.map(function deleteNote(note) {
|
||||||
.map(function deleteNote (note) {
|
|
||||||
return deleteSingleNote(storageKey, note.key)
|
return deleteSingleNote(storageKey, note.key)
|
||||||
})
|
})
|
||||||
return Promise.all(deleteAllNotes)
|
return Promise.all(deleteAllNotes).then(() => storage)
|
||||||
.then(() => storage)
|
|
||||||
})
|
})
|
||||||
.then(function(storage) {
|
.then(function(storage) {
|
||||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'boostnote.json'),
|
||||||
|
_.pick(storage, ['folders', 'version'])
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
storage,
|
storage,
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ function deleteNote (storageKey, noteKey) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(function deleteAttachments(storageInfo) {
|
.then(function deleteAttachments(storageInfo) {
|
||||||
attachmentManagement.deleteAttachmentFolder(storageInfo.storageKey, storageInfo.noteKey)
|
attachmentManagement.deleteAttachmentFolder(
|
||||||
|
storageInfo.storageKey,
|
||||||
|
storageInfo.noteKey
|
||||||
|
)
|
||||||
return storageInfo
|
return storageInfo
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
|
|||||||
|
|
||||||
function deleteSnippet(snippet, snippetFile) {
|
function deleteSnippet(snippet, snippetFile) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fetchSnippet(null, snippetFile).then((snippets) => {
|
fetchSnippet(null, snippetFile).then(snippets => {
|
||||||
snippets = snippets.filter(currentSnippet => currentSnippet.id !== snippet.id)
|
snippets = snippets.filter(
|
||||||
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
currentSnippet => currentSnippet.id !== snippet.id
|
||||||
|
)
|
||||||
|
fs.writeFile(
|
||||||
|
snippetFile || consts.SNIPPET_FILE,
|
||||||
|
JSON.stringify(snippets, null, 4),
|
||||||
|
err => {
|
||||||
if (err) reject(err)
|
if (err) reject(err)
|
||||||
resolve(snippet)
|
resolve(snippet)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
|||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage)
|
||||||
.then(function assignNotes(storage) {
|
.then(function assignNotes(storage) {
|
||||||
return resolveStorageNotes(storage)
|
return resolveStorageNotes(storage).then(notes => {
|
||||||
.then((notes) => {
|
|
||||||
return {
|
return {
|
||||||
storage,
|
storage,
|
||||||
notes
|
notes
|
||||||
@@ -43,11 +42,26 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
|||||||
.then(function exportNotes(data) {
|
.then(function exportNotes(data) {
|
||||||
const { storage, notes } = data
|
const { storage, notes } = data
|
||||||
|
|
||||||
return Promise.all(notes
|
return Promise.all(
|
||||||
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
notes
|
||||||
|
.filter(
|
||||||
|
note =>
|
||||||
|
note.folder === folderKey &&
|
||||||
|
note.isTrashed === false &&
|
||||||
|
note.type === 'MARKDOWN_NOTE'
|
||||||
|
)
|
||||||
.map(note => {
|
.map(note => {
|
||||||
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
|
const notePath = path.join(
|
||||||
return exportNote(note.key, storage.path, note.content, notePath, null)
|
exportDir,
|
||||||
|
`${filenamify(note.title, { replacement: '_' })}.${fileType}`
|
||||||
|
)
|
||||||
|
return exportNote(
|
||||||
|
note.key,
|
||||||
|
storage.path,
|
||||||
|
note.content,
|
||||||
|
notePath,
|
||||||
|
null
|
||||||
|
)
|
||||||
})
|
})
|
||||||
).then(() => ({
|
).then(() => ({
|
||||||
storage,
|
storage,
|
||||||
|
|||||||
@@ -19,8 +19,16 @@ const attachmentManagement = require('./attachmentManagement')
|
|||||||
* @param {function} outputFormatter
|
* @param {function} outputFormatter
|
||||||
* @return {Promise.<*[]>}
|
* @return {Promise.<*[]>}
|
||||||
*/
|
*/
|
||||||
function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) {
|
function exportNote(
|
||||||
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
nodeKey,
|
||||||
|
storageKey,
|
||||||
|
noteContent,
|
||||||
|
targetPath,
|
||||||
|
outputFormatter
|
||||||
|
) {
|
||||||
|
const storagePath = path.isAbsolute(storageKey)
|
||||||
|
? storageKey
|
||||||
|
: findStorage(storageKey).path
|
||||||
const exportTasks = []
|
const exportTasks = []
|
||||||
|
|
||||||
if (!storagePath) {
|
if (!storagePath) {
|
||||||
@@ -50,18 +58,19 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt
|
|||||||
|
|
||||||
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
||||||
|
|
||||||
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
|
return Promise.all(tasks.map(task => copyFile(task.src, task.dst)))
|
||||||
.then(() => exportedData)
|
.then(() => exportedData)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
return saveToFile(data, targetPath)
|
return saveToFile(data, targetPath)
|
||||||
}).catch((err) => {
|
})
|
||||||
|
.catch(err => {
|
||||||
rollbackExport(tasks)
|
rollbackExport(tasks)
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareTasks(tasks, storagePath, targetPath) {
|
function prepareTasks(tasks, storagePath, targetPath) {
|
||||||
return tasks.map((task) => {
|
return tasks.map(task => {
|
||||||
if (!path.isAbsolute(task.src)) {
|
if (!path.isAbsolute(task.src)) {
|
||||||
task.src = path.join(storagePath, task.src)
|
task.src = path.join(storagePath, task.src)
|
||||||
}
|
}
|
||||||
@@ -76,7 +85,7 @@ function prepareTasks (tasks, storagePath, targetPath) {
|
|||||||
|
|
||||||
function saveToFile(data, filename) {
|
function saveToFile(data, filename) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.writeFile(filename, data, (err) => {
|
fs.writeFile(filename, data, err => {
|
||||||
if (err) return reject(err)
|
if (err) return reject(err)
|
||||||
|
|
||||||
resolve(filename)
|
resolve(filename)
|
||||||
@@ -90,7 +99,7 @@ function saveToFile (data, filename) {
|
|||||||
*/
|
*/
|
||||||
function rollbackExport(tasks) {
|
function rollbackExport(tasks) {
|
||||||
const folders = new Set()
|
const folders = new Set()
|
||||||
tasks.forEach((task) => {
|
tasks.forEach(task => {
|
||||||
let fullpath = task.dst
|
let fullpath = task.dst
|
||||||
|
|
||||||
if (!path.extname(task.dst)) {
|
if (!path.extname(task.dst)) {
|
||||||
@@ -103,7 +112,7 @@ function rollbackExport (tasks) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
folders.forEach((folder) => {
|
folders.forEach(folder => {
|
||||||
if (fs.readdirSync(folder).length === 0) {
|
if (fs.readdirSync(folder).length === 0) {
|
||||||
fs.rmdir(folder)
|
fs.rmdir(folder)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,14 +29,17 @@ function exportStorage (storageKey, fileType, exportDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage)
|
||||||
.then(storage => (
|
.then(storage =>
|
||||||
resolveStorageNotes(storage).then(notes => ({ storage, notes }))
|
resolveStorageNotes(storage).then(notes => ({ storage, notes }))
|
||||||
))
|
)
|
||||||
.then(function exportNotes(data) {
|
.then(function exportNotes(data) {
|
||||||
const { storage, notes } = data
|
const { storage, notes } = data
|
||||||
const folderNamesMapping = {}
|
const folderNamesMapping = {}
|
||||||
storage.folders.forEach(folder => {
|
storage.folders.forEach(folder => {
|
||||||
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
|
const folderExportedDir = path.join(
|
||||||
|
exportDir,
|
||||||
|
filenamify(folder.name, { replacement: '_' })
|
||||||
|
)
|
||||||
folderNamesMapping[folder.key] = folderExportedDir
|
folderNamesMapping[folder.key] = folderExportedDir
|
||||||
// make sure directory exists
|
// make sure directory exists
|
||||||
try {
|
try {
|
||||||
@@ -47,7 +50,9 @@ function exportStorage (storageKey, fileType, exportDir) {
|
|||||||
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
|
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
|
||||||
.forEach(markdownNote => {
|
.forEach(markdownNote => {
|
||||||
const folderExportedDir = folderNamesMapping[markdownNote.folder]
|
const folderExportedDir = folderNamesMapping[markdownNote.folder]
|
||||||
const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}`
|
const snippetName = `${filenamify(markdownNote.title, {
|
||||||
|
replacement: '_'
|
||||||
|
})}.${fileType}`
|
||||||
const notePath = path.join(folderExportedDir, snippetName)
|
const notePath = path.join(folderExportedDir, snippetName)
|
||||||
fs.writeFileSync(notePath, markdownNote.content)
|
fs.writeFileSync(notePath, markdownNote.content)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ function fetchSnippet (id, snippetFile) {
|
|||||||
}
|
}
|
||||||
const snippets = JSON.parse(data)
|
const snippets = JSON.parse(data)
|
||||||
if (id) {
|
if (id) {
|
||||||
const snippet = snippets.find(snippet => { return snippet.id === id })
|
const snippet = snippets.find(snippet => {
|
||||||
|
return snippet.id === id
|
||||||
|
})
|
||||||
resolve(snippet)
|
resolve(snippet)
|
||||||
}
|
}
|
||||||
resolve(snippets)
|
resolve(snippets)
|
||||||
|
|||||||
@@ -34,19 +34,20 @@ function init () {
|
|||||||
rawStorages = []
|
rawStorages = []
|
||||||
window.localStorage.setItem('storages', JSON.stringify(rawStorages))
|
window.localStorage.setItem('storages', JSON.stringify(rawStorages))
|
||||||
}
|
}
|
||||||
return Promise.all(rawStorages
|
return Promise.all(rawStorages.map(resolveStorageData))
|
||||||
.map(resolveStorageData))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchNotes = function(storages) {
|
const fetchNotes = function(storages) {
|
||||||
const findNotesFromEachStorage = storages
|
const findNotesFromEachStorage = storages
|
||||||
.filter(storage => fs.existsSync(storage.path))
|
.filter(storage => fs.existsSync(storage.path))
|
||||||
.map((storage) => {
|
.map(storage => {
|
||||||
return resolveStorageNotes(storage)
|
return resolveStorageNotes(storage).then(notes => {
|
||||||
.then((notes) => {
|
|
||||||
let unknownCount = 0
|
let unknownCount = 0
|
||||||
notes.forEach((note) => {
|
notes.forEach(note => {
|
||||||
if (note && !storage.folders.some((folder) => note.folder === folder.key)) {
|
if (
|
||||||
|
note &&
|
||||||
|
!storage.folders.some(folder => note.folder === folder.key)
|
||||||
|
) {
|
||||||
unknownCount++
|
unknownCount++
|
||||||
storage.folders.push({
|
storage.folders.push({
|
||||||
key: note.folder,
|
key: note.folder,
|
||||||
@@ -57,9 +58,14 @@ function init () {
|
|||||||
})
|
})
|
||||||
if (unknownCount > 0) {
|
if (unknownCount > 0) {
|
||||||
try {
|
try {
|
||||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'boostnote.json'),
|
||||||
|
_.pick(storage, ['folders', 'version'])
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Error writting boostnote.json: ' + e + ' from init.js')
|
console.log(
|
||||||
|
'Error writting boostnote.json: ' + e + ' from init.js'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return notes
|
return notes
|
||||||
@@ -80,9 +86,8 @@ function init () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(fetchStorages())
|
return Promise.resolve(fetchStorages())
|
||||||
.then((storages) => {
|
.then(storages => {
|
||||||
return storages
|
return storages.filter(storage => {
|
||||||
.filter((storage) => {
|
|
||||||
if (!_.isObject(storage)) return false
|
if (!_.isObject(storage)) return false
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ function migrateFromV5Storage (storageKey, data) {
|
|||||||
let targetStorage
|
let targetStorage
|
||||||
try {
|
try {
|
||||||
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
if (!_.isArray(cachedStorageList))
|
||||||
|
throw new Error("Target storage doesn't exist.")
|
||||||
|
|
||||||
targetStorage = _.find(cachedStorageList, { key: storageKey })
|
targetStorage = _.find(cachedStorageList, { key: storageKey })
|
||||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
if (targetStorage == null) throw new Error("Target storage doesn't exist.")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
}
|
}
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage).then(function(storage) {
|
||||||
.then(function (storage) {
|
|
||||||
return importAll(storage, data)
|
return importAll(storage, data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -26,10 +26,9 @@ function migrateFromV5Storage (storageKey, data) {
|
|||||||
function importAll(storage, data) {
|
function importAll(storage, data) {
|
||||||
const oldArticles = data.articles
|
const oldArticles = data.articles
|
||||||
const notes = []
|
const notes = []
|
||||||
data.folders
|
data.folders.forEach(function(oldFolder) {
|
||||||
.forEach(function (oldFolder) {
|
|
||||||
let folderKey = keygen()
|
let folderKey = keygen()
|
||||||
while (storage.folders.some((folder) => folder.key === folderKey)) {
|
while (storage.folders.some(folder => folder.key === folderKey)) {
|
||||||
folderKey = keygen()
|
folderKey = keygen()
|
||||||
}
|
}
|
||||||
const newFolder = {
|
const newFolder = {
|
||||||
@@ -40,8 +39,10 @@ function importAll (storage, data) {
|
|||||||
|
|
||||||
storage.folders.push(newFolder)
|
storage.folders.push(newFolder)
|
||||||
|
|
||||||
const articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
|
const articles = oldArticles.filter(
|
||||||
articles.forEach((article) => {
|
article => article.FolderKey === oldFolder.key
|
||||||
|
)
|
||||||
|
articles.forEach(article => {
|
||||||
let noteKey = keygen()
|
let noteKey = keygen()
|
||||||
let isUnique = false
|
let isUnique = false
|
||||||
while (!isUnique) {
|
while (!isUnique) {
|
||||||
@@ -85,12 +86,14 @@ function importAll (storage, data) {
|
|||||||
title: article.title,
|
title: article.title,
|
||||||
description: article.title,
|
description: article.title,
|
||||||
key: noteKey,
|
key: noteKey,
|
||||||
snippets: [{
|
snippets: [
|
||||||
|
{
|
||||||
name: article.mode,
|
name: article.mode,
|
||||||
mode: article.mode,
|
mode: article.mode,
|
||||||
content: article.content,
|
content: article.content,
|
||||||
linesHighlighted: article.linesHighlighted
|
linesHighlighted: article.linesHighlighted
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
notes.push(newNote)
|
notes.push(newNote)
|
||||||
}
|
}
|
||||||
@@ -98,10 +101,16 @@ function importAll (storage, data) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
notes.forEach(function(note) {
|
notes.forEach(function(note) {
|
||||||
CSON.writeFileSync(path.join(storage.path, 'notes', note.key + '.cson'), _.omit(note, ['storage', 'key']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'notes', note.key + '.cson'),
|
||||||
|
_.omit(note, ['storage', 'key'])
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['version', 'folders']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'boostnote.json'),
|
||||||
|
_.pick(storage, ['version', 'folders'])
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
storage,
|
storage,
|
||||||
|
|||||||
@@ -14,29 +14,37 @@ function migrateFromV5Storage (storagePath) {
|
|||||||
})
|
})
|
||||||
.then(function verifyVersion(rawData) {
|
.then(function verifyVersion(rawData) {
|
||||||
var boostnoteJSONData = JSON.parse(rawData)
|
var boostnoteJSONData = JSON.parse(rawData)
|
||||||
if (boostnoteJSONData.version === '1.0') throw new Error('Target storage seems to be transformed already.')
|
if (boostnoteJSONData.version === '1.0')
|
||||||
if (!_.isArray(boostnoteJSONData.folders)) throw new Error('the value of folders is not an array.')
|
throw new Error('Target storage seems to be transformed already.')
|
||||||
|
if (!_.isArray(boostnoteJSONData.folders))
|
||||||
|
throw new Error('the value of folders is not an array.')
|
||||||
|
|
||||||
return boostnoteJSONData
|
return boostnoteJSONData
|
||||||
})
|
})
|
||||||
.then(function setVersion(boostnoteJSONData) {
|
.then(function setVersion(boostnoteJSONData) {
|
||||||
boostnoteJSONData.version = '1.0'
|
boostnoteJSONData.version = '1.0'
|
||||||
return sander.writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData))
|
return sander
|
||||||
|
.writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData))
|
||||||
.then(() => boostnoteJSONData)
|
.then(() => boostnoteJSONData)
|
||||||
})
|
})
|
||||||
.then(function fetchNotes(boostnoteJSONData) {
|
.then(function fetchNotes(boostnoteJSONData) {
|
||||||
var fetchNotesFromEachFolder = boostnoteJSONData.folders
|
var fetchNotesFromEachFolder = boostnoteJSONData.folders.map(function(
|
||||||
.map(function (folder) {
|
folder
|
||||||
const folderDataJSONPath = path.join(storagePath, folder.key, 'data.json')
|
) {
|
||||||
|
const folderDataJSONPath = path.join(
|
||||||
|
storagePath,
|
||||||
|
folder.key,
|
||||||
|
'data.json'
|
||||||
|
)
|
||||||
return sander
|
return sander
|
||||||
.readFile(folderDataJSONPath, {
|
.readFile(folderDataJSONPath, {
|
||||||
encoding: 'utf-8'
|
encoding: 'utf-8'
|
||||||
})
|
})
|
||||||
.then(function(rawData) {
|
.then(function(rawData) {
|
||||||
var data = JSON.parse(rawData)
|
var data = JSON.parse(rawData)
|
||||||
if (!_.isArray(data.notes)) throw new Error('value of notes is not an array.')
|
if (!_.isArray(data.notes))
|
||||||
return data.notes
|
throw new Error('value of notes is not an array.')
|
||||||
.map(function setFolderToNote (note) {
|
return data.notes.map(function setFolderToNote(note) {
|
||||||
note.folder = folder.key
|
note.folder = folder.key
|
||||||
return note
|
return note
|
||||||
})
|
})
|
||||||
@@ -49,30 +57,28 @@ function migrateFromV5Storage (storagePath) {
|
|||||||
|
|
||||||
return Promise.all(fetchNotesFromEachFolder)
|
return Promise.all(fetchNotesFromEachFolder)
|
||||||
.then(function flatten(folderNotes) {
|
.then(function flatten(folderNotes) {
|
||||||
return folderNotes
|
return folderNotes.reduce(function concatNotes(sum, notes) {
|
||||||
.reduce(function concatNotes (sum, notes) {
|
|
||||||
return sum.concat(notes)
|
return sum.concat(notes)
|
||||||
}, [])
|
}, [])
|
||||||
})
|
})
|
||||||
.then(function saveNotes(notes) {
|
.then(function saveNotes(notes) {
|
||||||
notes.forEach(function renewKey(note) {
|
notes.forEach(function renewKey(note) {
|
||||||
var newKey = keygen()
|
var newKey = keygen()
|
||||||
while (notes.some((_note) => _note.key === newKey)) {
|
while (notes.some(_note => _note.key === newKey)) {
|
||||||
newKey = keygen()
|
newKey = keygen()
|
||||||
}
|
}
|
||||||
note.key = newKey
|
note.key = newKey
|
||||||
})
|
})
|
||||||
|
|
||||||
const noteDirPath = path.join(storagePath, 'notes')
|
const noteDirPath = path.join(storagePath, 'notes')
|
||||||
notes
|
notes.map(function saveNote(note) {
|
||||||
.map(function saveNote (note) {
|
|
||||||
CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note)
|
CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note)
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.then(function deleteFolderDir(check) {
|
.then(function deleteFolderDir(check) {
|
||||||
if (check) {
|
if (check) {
|
||||||
boostnoteJSONData.folders.forEach((folder) => {
|
boostnoteJSONData.folders.forEach(folder => {
|
||||||
sander.rimrafSync(path.join(storagePath, folder.key))
|
sander.rimrafSync(path.join(storagePath, folder.key))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -86,4 +92,3 @@ function migrateFromV5Storage (storagePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = migrateFromV5Storage
|
module.exports = migrateFromV5Storage
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,12 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
|||||||
try {
|
try {
|
||||||
oldStorage = findStorage(storageKey)
|
oldStorage = findStorage(storageKey)
|
||||||
newStorage = findStorage(newStorageKey)
|
newStorage = findStorage(newStorageKey)
|
||||||
if (newStorage == null) throw new Error('Target storage doesn\'t exist.')
|
if (newStorage == null) throw new Error("Target storage doesn't exist.")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveStorageData(oldStorage)
|
return resolveStorageData(oldStorage).then(function saveNote(_oldStorage) {
|
||||||
.then(function saveNote (_oldStorage) {
|
|
||||||
oldStorage = _oldStorage
|
oldStorage = _oldStorage
|
||||||
let noteData
|
let noteData
|
||||||
const notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
|
const notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
|
||||||
@@ -35,14 +34,17 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
|||||||
newNoteKey = noteKey
|
newNoteKey = noteKey
|
||||||
return oldStorage
|
return oldStorage
|
||||||
}
|
}
|
||||||
return resolveStorageData(newStorage)
|
return resolveStorageData(newStorage).then(function findNewNoteKey(
|
||||||
.then(function findNewNoteKey (_newStorage) {
|
_newStorage
|
||||||
|
) {
|
||||||
newStorage = _newStorage
|
newStorage = _newStorage
|
||||||
newNoteKey = keygen(true)
|
newNoteKey = keygen(true)
|
||||||
let isUnique = false
|
let isUnique = false
|
||||||
while (!isUnique) {
|
while (!isUnique) {
|
||||||
try {
|
try {
|
||||||
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
|
sander.statSync(
|
||||||
|
path.join(newStorage.path, 'notes', newNoteKey + '.cson')
|
||||||
|
)
|
||||||
newNoteKey = keygen(true)
|
newNoteKey = keygen(true)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
@@ -57,7 +59,8 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(function checkFolderExistsAndPrepareNoteData(newStorage) {
|
.then(function checkFolderExistsAndPrepareNoteData(newStorage) {
|
||||||
if (_.find(newStorage.folders, {key: newFolderKey}) == null) throw new Error('Target folder doesn\'t exist.')
|
if (_.find(newStorage.folders, { key: newFolderKey }) == null)
|
||||||
|
throw new Error("Target folder doesn't exist.")
|
||||||
|
|
||||||
noteData.folder = newFolderKey
|
noteData.folder = newFolderKey
|
||||||
noteData.key = newNoteKey
|
noteData.key = newNoteKey
|
||||||
@@ -72,17 +75,28 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
|||||||
return noteData
|
return noteData
|
||||||
}
|
}
|
||||||
|
|
||||||
noteData.content = attachmentManagement.moveAttachments(oldStorage.path, newStorage.path, noteKey, newNoteKey, noteData.content)
|
noteData.content = attachmentManagement.moveAttachments(
|
||||||
|
oldStorage.path,
|
||||||
|
newStorage.path,
|
||||||
|
noteKey,
|
||||||
|
newNoteKey,
|
||||||
|
noteData.content
|
||||||
|
)
|
||||||
return noteData
|
return noteData
|
||||||
})
|
})
|
||||||
.then(function writeAndReturn(noteData) {
|
.then(function writeAndReturn(noteData) {
|
||||||
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage', 'oldContent']))
|
CSON.writeFileSync(
|
||||||
|
path.join(newStorage.path, 'notes', noteData.key + '.cson'),
|
||||||
|
_.omit(noteData, ['key', 'storage', 'oldContent'])
|
||||||
|
)
|
||||||
return noteData
|
return noteData
|
||||||
})
|
})
|
||||||
.then(function deleteOldNote(data) {
|
.then(function deleteOldNote(data) {
|
||||||
if (storageKey !== newStorageKey) {
|
if (storageKey !== newStorageKey) {
|
||||||
try {
|
try {
|
||||||
sander.unlinkSync(path.join(oldStorage.path, 'notes', noteKey + '.cson'))
|
sander.unlinkSync(
|
||||||
|
path.join(oldStorage.path, 'notes', noteKey + '.cson')
|
||||||
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ function removeStorage (key) {
|
|||||||
rawStorages = []
|
rawStorages = []
|
||||||
}
|
}
|
||||||
|
|
||||||
rawStorages = rawStorages
|
rawStorages = rawStorages.filter(function excludeTargetStorage(rawStorage) {
|
||||||
.filter(function excludeTargetStorage (rawStorage) {
|
|
||||||
return rawStorage.key !== key
|
return rawStorage.key !== key
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const resolveStorageData = require('./resolveStorageData')
|
|||||||
* @return {Object} Storage meta data
|
* @return {Object} Storage meta data
|
||||||
*/
|
*/
|
||||||
function renameStorage(key, name) {
|
function renameStorage(key, name) {
|
||||||
if (!_.isString(name)) return Promise.reject(new Error('Name must be a string.'))
|
if (!_.isString(name))
|
||||||
|
return Promise.reject(new Error('Name must be a string.'))
|
||||||
|
|
||||||
let cachedStorageList
|
let cachedStorageList
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -28,10 +28,14 @@ function reorderFolder (storageKey, oldIndex, newIndex) {
|
|||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage).then(function reorderFolder(
|
||||||
.then(function reorderFolder (storage) {
|
storage
|
||||||
|
) {
|
||||||
storage.folders = _.move(storage.folders, oldIndex, newIndex)
|
storage.folders = _.move(storage.folders, oldIndex, newIndex)
|
||||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'boostnote.json'),
|
||||||
|
_.pick(storage, ['folders', 'version'])
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
storage
|
storage
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ function resolveStorageData (storageCache) {
|
|||||||
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
|
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
|
||||||
try {
|
try {
|
||||||
const jsonData = CSON.readFileSync(boostnoteJSONPath)
|
const jsonData = CSON.readFileSync(boostnoteJSONPath)
|
||||||
if (!_.isArray(jsonData.folders)) throw new Error('folders should be an array.')
|
if (!_.isArray(jsonData.folders))
|
||||||
|
throw new Error('folders should be an array.')
|
||||||
storage.folders = jsonData.folders
|
storage.folders = jsonData.folders
|
||||||
storage.version = jsonData.version
|
storage.version = jsonData.version
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
console.warn('boostnote.json file doesn\'t exist the given path')
|
console.warn("boostnote.json file doesn't exist the given path")
|
||||||
CSON.writeFileSync(boostnoteJSONPath, { folders: [], version: '1.0' })
|
CSON.writeFileSync(boostnoteJSONPath, { folders: [], version: '1.0' })
|
||||||
} else {
|
} else {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@@ -34,8 +35,7 @@ function resolveStorageData (storageCache) {
|
|||||||
return Promise.resolve(storage)
|
return Promise.resolve(storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return migrateFromV6Storage(storage.path)
|
return migrateFromV6Storage(storage.path).then(() => storage)
|
||||||
.then(() => storage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolveStorageData
|
module.exports = resolveStorageData
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ function resolveStorageNotes (storage) {
|
|||||||
notePathList = sander.readdirSync(notesDirPath)
|
notePathList = sander.readdirSync(notesDirPath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
console.error(notesDirPath, ' doesn\'t exist.')
|
console.error(notesDirPath, " doesn't exist.")
|
||||||
sander.mkdirSync(notesDirPath)
|
sander.mkdirSync(notesDirPath)
|
||||||
} else {
|
} else {
|
||||||
console.warn('Failed to find note dir', notesDirPath, err)
|
console.warn('Failed to find note dir', notesDirPath, err)
|
||||||
|
|||||||
@@ -34,14 +34,16 @@ function updateFolder (storageKey, folderKey, input) {
|
|||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage).then(function updateFolder(storage) {
|
||||||
.then(function updateFolder (storage) {
|
|
||||||
const targetFolder = _.find(storage.folders, { key: folderKey })
|
const targetFolder = _.find(storage.folders, { key: folderKey })
|
||||||
if (targetFolder == null) throw new Error('Target folder doesn\'t exist.')
|
if (targetFolder == null) throw new Error("Target folder doesn't exist.")
|
||||||
targetFolder.name = input.name
|
targetFolder.name = input.name
|
||||||
targetFolder.color = input.color
|
targetFolder.color = input.color
|
||||||
|
|
||||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'boostnote.json'),
|
||||||
|
_.pick(storage, ['folders', 'version'])
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
storage
|
storage
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ function validateInput (input) {
|
|||||||
|
|
||||||
if (input.tags != null) {
|
if (input.tags != null) {
|
||||||
if (!_.isArray(input.tags)) validatedInput.tags = []
|
if (!_.isArray(input.tags)) validatedInput.tags = []
|
||||||
validatedInput.tags = input.tags
|
validatedInput.tags = input.tags.filter(
|
||||||
.filter((tag) => _.isString(tag) && tag.trim().length > 0)
|
tag => _.isString(tag) && tag.trim().length > 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.title != null) {
|
if (input.title != null) {
|
||||||
@@ -40,7 +41,8 @@ function validateInput (input) {
|
|||||||
if (!_.isString(input.content)) validatedInput.content = ''
|
if (!_.isString(input.content)) validatedInput.content = ''
|
||||||
else validatedInput.content = input.content
|
else validatedInput.content = input.content
|
||||||
|
|
||||||
if (!_.isArray(input.linesHighlighted)) validatedInput.linesHighlighted = []
|
if (!_.isArray(input.linesHighlighted))
|
||||||
|
validatedInput.linesHighlighted = []
|
||||||
else validatedInput.linesHighlighted = input.linesHighlighted
|
else validatedInput.linesHighlighted = input.linesHighlighted
|
||||||
}
|
}
|
||||||
return validatedInput
|
return validatedInput
|
||||||
@@ -51,17 +53,18 @@ function validateInput (input) {
|
|||||||
}
|
}
|
||||||
if (input.snippets != null) {
|
if (input.snippets != null) {
|
||||||
if (!_.isArray(input.snippets)) {
|
if (!_.isArray(input.snippets)) {
|
||||||
validatedInput.snippets = [{
|
validatedInput.snippets = [
|
||||||
|
{
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'text',
|
mode: 'text',
|
||||||
content: '',
|
content: '',
|
||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
} else {
|
} else {
|
||||||
validatedInput.snippets = input.snippets
|
validatedInput.snippets = input.snippets
|
||||||
}
|
}
|
||||||
validatedInput.snippets
|
validatedInput.snippets.filter(snippet => {
|
||||||
.filter((snippet) => {
|
|
||||||
if (!_.isString(snippet.name)) return false
|
if (!_.isString(snippet.name)) return false
|
||||||
if (!_.isString(snippet.mode)) return false
|
if (!_.isString(snippet.mode)) return false
|
||||||
if (!_.isString(snippet.content)) return false
|
if (!_.isString(snippet.content)) return false
|
||||||
@@ -70,7 +73,9 @@ function validateInput (input) {
|
|||||||
}
|
}
|
||||||
return validatedInput
|
return validatedInput
|
||||||
default:
|
default:
|
||||||
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
|
throw new Error(
|
||||||
|
'Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,24 +90,26 @@ function updateNote (storageKey, noteKey, input) {
|
|||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage).then(function saveNote(storage) {
|
||||||
.then(function saveNote (storage) {
|
|
||||||
let noteData
|
let noteData
|
||||||
const notePath = path.join(storage.path, 'notes', noteKey + '.cson')
|
const notePath = path.join(storage.path, 'notes', noteKey + '.cson')
|
||||||
try {
|
try {
|
||||||
noteData = CSON.readFileSync(notePath)
|
noteData = CSON.readFileSync(notePath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to find note cson', err)
|
console.warn('Failed to find note cson', err)
|
||||||
noteData = input.type === 'SNIPPET_NOTE'
|
noteData =
|
||||||
|
input.type === 'SNIPPET_NOTE'
|
||||||
? {
|
? {
|
||||||
type: 'SNIPPET_NOTE',
|
type: 'SNIPPET_NOTE',
|
||||||
description: [],
|
description: [],
|
||||||
snippets: [{
|
snippets: [
|
||||||
|
{
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'text',
|
mode: 'text',
|
||||||
content: '',
|
content: '',
|
||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'MARKDOWN_NOTE',
|
type: 'MARKDOWN_NOTE',
|
||||||
@@ -110,7 +117,8 @@ function updateNote (storageKey, noteKey, input) {
|
|||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
noteData.title = ''
|
noteData.title = ''
|
||||||
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')
|
if (storage.folders.length === 0)
|
||||||
|
throw new Error('Failed to restore note: No folder exists.')
|
||||||
noteData.folder = storage.folders[0].key
|
noteData.folder = storage.folders[0].key
|
||||||
noteData.createdAt = new Date()
|
noteData.createdAt = new Date()
|
||||||
noteData.updatedAt = new Date()
|
noteData.updatedAt = new Date()
|
||||||
@@ -130,7 +138,10 @@ function updateNote (storageKey, noteKey, input) {
|
|||||||
storage: storageKey
|
storage: storageKey
|
||||||
})
|
})
|
||||||
|
|
||||||
CSON.writeFileSync(path.join(storage.path, 'notes', noteKey + '.cson'), _.omit(noteData, ['key', 'storage']))
|
CSON.writeFileSync(
|
||||||
|
path.join(storage.path, 'notes', noteKey + '.cson'),
|
||||||
|
_.omit(noteData, ['key', 'storage'])
|
||||||
|
)
|
||||||
|
|
||||||
return noteData
|
return noteData
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import consts from 'browser/lib/consts'
|
|||||||
|
|
||||||
function updateSnippet(snippet, snippetFile) {
|
function updateSnippet(snippet, snippetFile) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const snippets = JSON.parse(fs.readFileSync(snippetFile || consts.SNIPPET_FILE, 'utf-8'))
|
const snippets = JSON.parse(
|
||||||
|
fs.readFileSync(snippetFile || consts.SNIPPET_FILE, 'utf-8')
|
||||||
|
)
|
||||||
|
|
||||||
for (let i = 0; i < snippets.length; i++) {
|
for (let i = 0; i < snippets.length; i++) {
|
||||||
const currentSnippet = snippets[i]
|
const currentSnippet = snippets[i]
|
||||||
@@ -21,11 +23,15 @@ function updateSnippet (snippet, snippetFile) {
|
|||||||
currentSnippet.name = snippet.name
|
currentSnippet.name = snippet.name
|
||||||
currentSnippet.prefix = snippet.prefix
|
currentSnippet.prefix = snippet.prefix
|
||||||
currentSnippet.content = snippet.content
|
currentSnippet.content = snippet.content
|
||||||
currentSnippet.linesHighlighted = (snippet.linesHighlighted)
|
currentSnippet.linesHighlighted = snippet.linesHighlighted
|
||||||
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
fs.writeFile(
|
||||||
|
snippetFile || consts.SNIPPET_FILE,
|
||||||
|
JSON.stringify(snippets, null, 4),
|
||||||
|
err => {
|
||||||
if (err) reject(err)
|
if (err) reject(err)
|
||||||
resolve(snippets)
|
resolve(snippets)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,29 @@ class ModalBase extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
|
if (modalBase != null)
|
||||||
|
modalBase.setState({
|
||||||
|
component: null,
|
||||||
|
componentProps: null,
|
||||||
|
isHidden: true
|
||||||
|
})
|
||||||
// Toggle overflow style on NoteList
|
// Toggle overflow style on NoteList
|
||||||
const list = document.querySelector('.NoteList__list___browser-main-NoteList-')
|
const list = document.querySelector(
|
||||||
|
'.NoteList__list___browser-main-NoteList-'
|
||||||
|
)
|
||||||
list.style.overflow = 'auto'
|
list.style.overflow = 'auto'
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
|
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
|
||||||
<div onClick={(e) => this.close(e)} className='modalBack' />
|
<div onClick={e => this.close(e)} className='modalBack' />
|
||||||
{this.state.component == null ? null : (
|
{this.state.component == null ? null : (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<this.state.component {...this.state.componentProps} close={this.close} />
|
<this.state.component
|
||||||
|
{...this.state.componentProps}
|
||||||
|
close={this.close}
|
||||||
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -39,16 +49,26 @@ document.body.appendChild(el)
|
|||||||
const modalBase = ReactDOM.render(<ModalBase />, el)
|
const modalBase = ReactDOM.render(<ModalBase />, el)
|
||||||
|
|
||||||
export function openModal(component, props) {
|
export function openModal(component, props) {
|
||||||
if (modalBase == null) { return }
|
if (modalBase == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// Hide scrollbar by removing overflow when modal opens
|
// Hide scrollbar by removing overflow when modal opens
|
||||||
const list = document.querySelector('.NoteList__list___browser-main-NoteList-')
|
const list = document.querySelector(
|
||||||
|
'.NoteList__list___browser-main-NoteList-'
|
||||||
|
)
|
||||||
list.style.overflow = 'hidden'
|
list.style.overflow = 'hidden'
|
||||||
document.body.setAttribute('data-modal', 'open')
|
document.body.setAttribute('data-modal', 'open')
|
||||||
modalBase.setState({component: component, componentProps: props, isHidden: false})
|
modalBase.setState({
|
||||||
|
component: component,
|
||||||
|
componentProps: props,
|
||||||
|
isHidden: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeModal() {
|
export function closeModal() {
|
||||||
if (modalBase == null) { return }
|
if (modalBase == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
modalBase.close()
|
modalBase.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user