1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-11 00:36:26 +00:00

fixed eslint error & integrated with prettier as well as formatted the whole codebase (#3450)

This commit is contained in:
Nguyen Viet Hung
2020-02-05 13:28:27 +13:00
committed by GitHub
parent 051ce9e208
commit 592aca1539
186 changed files with 9233 additions and 5565 deletions

View File

@@ -1,6 +1,6 @@
{
"extends": ["standard", "standard-jsx", "plugin:react/recommended"],
"plugins": ["react"],
"extends": ["standard", "standard-jsx", "plugin:react/recommended", "prettier"],
"plugins": ["react", "prettier"],
"rules": {
"no-useless-escape": 0,
"prefer-const": ["warn", {
@@ -13,7 +13,8 @@
"react/no-string-refs": 0,
"react/no-find-dom-node": "warn",
"react/no-render-return-value": "warn",
"react/no-deprecated": "warn"
"react/no-deprecated": "warn",
"prettier/prettier": ["error"]
},
"globals": {
"FileReader": true,

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"singleQuote": true,
"semi": false,
"jsxSingleQuote": true
}

View File

@@ -6,11 +6,7 @@ import hljs from 'highlight.js'
import 'codemirror-mode-elixir'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import convertModeName from 'browser/lib/convertModeName'
import {
options,
TableEditor,
Alignment
} from '@susisu/mte-kernel'
import { options, TableEditor, Alignment } from '@susisu/mte-kernel'
import TextEditorInterface from 'browser/lib/TextEditorInterface'
import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
@@ -20,11 +16,15 @@ import styles from '../components/CodeEditor.styl'
const { ipcRenderer, remote, clipboard } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
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 {languageMaps} from '../lib/CMLanguageList'
import { languageMaps } from '../lib/CMLanguageList'
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 Jsonlint from 'jsonlint-mod'
import { DEFAULT_CONFIG } from '../main/lib/ConfigManager'
@@ -33,28 +33,38 @@ import prettier from 'prettier'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const buildCMRulers = (rulers, enableRulers) =>
(enableRulers ? rulers.map(ruler => ({
column: ruler
})) : [])
enableRulers
? rulers.map(ruler => ({
column: ruler
}))
: []
function translateHotkey (hotkey) {
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
function translateHotkey(hotkey) {
return hotkey
.replace(/\s*\+\s*/g, '-')
.replace(/Command/g, 'Cmd')
.replace(/Control/g, 'Ctrl')
}
export default class CodeEditor extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
leading: false,
trailing: true
})
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
this.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject)
this.changeHandler = (editor, changeObject) =>
this.handleChange(editor, changeObject)
this.highlightHandler = (editor, changeObject) =>
this.handleHighlight(editor, changeObject)
this.focusHandler = () => {
ipcRenderer.send('editor:focused', true)
}
const debouncedDeletionOfAttachments = _.debounce(attachmentManagement.deleteAttachmentsNotPresentInNote, 30000)
const debouncedDeletionOfAttachments = _.debounce(
attachmentManagement.deleteAttachmentsNotPresentInNote,
30000
)
this.blurHandler = (editor, e) => {
ipcRenderer.send('editor:focused', false)
if (e == null) return null
@@ -66,12 +76,13 @@ export default class CodeEditor extends React.Component {
el = el.parentNode
}
this.props.onBlur != null && this.props.onBlur(e)
const {
storageKey,
noteKey
} = this.props
const { storageKey, noteKey } = this.props
if (this.props.deleteUnusedAttachments === true) {
debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey)
debouncedDeletionOfAttachments(
this.editor.getValue(),
storageKey,
noteKey
)
}
}
this.pasteHandler = (editor, e) => {
@@ -91,7 +102,7 @@ export default class CodeEditor extends React.Component {
this.formatTable = () => this.handleFormatTable()
if (props.switchPreview !== 'RIGHTCLICK') {
this.contextMenuHandler = function (editor, event) {
this.contextMenuHandler = function(editor, event) {
const menu = buildEditorContextMenu(editor, event)
if (menu != null) {
setTimeout(() => menu.popup(remote.getCurrentWindow()), 30)
@@ -104,24 +115,24 @@ export default class CodeEditor extends React.Component {
this.turndownService = createTurndownService()
}
handleSearch (msg) {
handleSearch(msg) {
const cm = this.editor
const component = this
if (component.searchState) cm.removeOverlay(component.searchState)
if (msg.length < 1) return
cm.operation(function () {
cm.operation(function() {
component.searchState = makeOverlay(msg, 'searching')
cm.addOverlay(component.searchState)
function makeOverlay (query, style) {
function makeOverlay(query, style) {
query = new RegExp(
query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
'gi'
)
return {
token: function (stream) {
token: function(stream) {
query.lastIndex = stream.pos
var match = query.exec(stream.string)
if (match && match.index === stream.pos) {
@@ -138,25 +149,27 @@ export default class CodeEditor extends React.Component {
})
}
handleFormatTable () {
this.tableEditor.formatAll(options({
textWidthOptions: {}
}))
handleFormatTable() {
this.tableEditor.formatAll(
options({
textWidthOptions: {}
})
)
}
handleEditorActivity () {
handleEditorActivity() {
if (!this.textEditorInterface.transaction) {
this.updateTableEditorState()
}
}
updateDefaultKeyMap () {
updateDefaultKeyMap() {
const { hotkey } = this.props
const self = this
const expandSnippet = snippetManager.expandSnippet
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
Tab: function (cm) {
Tab: function(cm) {
const cursor = cm.getCursor()
const line = cm.getLine(cursor.line)
const cursorPosition = cursor.ch
@@ -198,17 +211,17 @@ export default class CodeEditor extends React.Component {
}
}
},
'Cmd-Left': function (cm) {
'Cmd-Left': function(cm) {
cm.execCommand('goLineLeft')
},
'Cmd-T': function (cm) {
'Cmd-T': function(cm) {
// Do nothing
},
[translateHotkey(hotkey.insertDate)]: function (cm) {
[translateHotkey(hotkey.insertDate)]: function(cm) {
const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleDateString())
},
[translateHotkey(hotkey.insertDateTime)]: function (cm) {
[translateHotkey(hotkey.insertDateTime)]: function(cm) {
const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleString())
},
@@ -231,7 +244,10 @@ export default class CodeEditor extends React.Component {
currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos)
// 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 formattedCursorPos = formattedTextDetails.cursorOffset
@@ -246,7 +262,8 @@ export default class CodeEditor extends React.Component {
const appendLineBreak = /\n$/.test(selection)
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)
},
@@ -256,7 +273,7 @@ export default class CodeEditor extends React.Component {
})
}
updateTableEditorState () {
updateTableEditorState() {
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
if (active) {
if (this.extraKeysMode !== 'editor') {
@@ -272,7 +289,7 @@ export default class CodeEditor extends React.Component {
}
}
componentDidMount () {
componentDidMount() {
const { rulers, enableRulers, enableMarkdownLint, RTL } = this.props
eventEmitter.on('line:jump', this.scrollToLineHandeler)
@@ -298,7 +315,11 @@ export default class CodeEditor extends React.Component {
rtlMoveVisually: RTL,
foldGutter: true,
lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
gutters: [
'CodeMirror-linenumbers',
'CodeMirror-foldgutter',
'CodeMirror-lint-markers'
],
autoCloseBrackets: {
pairs: this.props.matchingPairs,
triples: this.props.matchingTriples,
@@ -309,7 +330,9 @@ export default class CodeEditor extends React.Component {
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) {
this.autoDetectLanguage(this.props.value)
@@ -342,7 +365,7 @@ export default class CodeEditor extends React.Component {
this.textEditorInterface = new TextEditorInterface(this.editor)
this.tableEditor = new TableEditor(this.textEditorInterface)
if (this.props.spellCheck) {
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
this.editor.addPanel(this.createSpellCheckPanel(), { position: 'bottom' })
}
eventEmitter.on('code:format-table', this.formatTable)
@@ -352,13 +375,13 @@ export default class CodeEditor extends React.Component {
})
this.editorKeyMap = CodeMirror.normalizeKeyMap({
'Tab': () => {
Tab: () => {
this.tableEditor.nextCell(this.tableEditorOptions)
},
'Shift-Tab': () => {
this.tableEditor.previousCell(this.tableEditorOptions)
},
'Enter': () => {
Enter: () => {
this.tableEditor.nextRow(this.tableEditorOptions)
},
'Ctrl-Enter': () => {
@@ -477,7 +500,7 @@ export default class CodeEditor extends React.Component {
this.initialHighlighting()
}
getWordBeforeCursor (line, lineNumber, cursorPosition) {
getWordBeforeCursor(line, lineNumber, cursorPosition) {
let wordBeforeCursor = ''
const originCursorPosition = cursorPosition
const emptyChars = /\t|\s|\r|\n|\$/
@@ -514,11 +537,11 @@ export default class CodeEditor extends React.Component {
}
}
quitEditor () {
quitEditor() {
document.querySelector('textarea').blur()
}
componentWillUnmount () {
componentWillUnmount() {
this.editor.off('focus', this.focusHandler)
this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler)
@@ -533,7 +556,7 @@ export default class CodeEditor extends React.Component {
eventEmitter.off('code:format-table', this.formatTable)
}
componentDidUpdate (prevProps, prevState) {
componentDidUpdate(prevProps, prevState) {
let needRefresh = false
const {
rulers,
@@ -561,13 +584,18 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('direction', this.props.RTL ? 'rtl' : 'ltr')
this.editor.setOption('rtlMoveVisually', this.props.RTL)
}
if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) {
if (
prevProps.enableMarkdownLint !== enableMarkdownLint ||
prevProps.customMarkdownLintConfig !== customMarkdownLintConfig
) {
if (!enableMarkdownLint) {
this.editor.setOption('lint', {default: false})
document.querySelector('.CodeMirror-lint-markers').style.display = 'none'
this.editor.setOption('lint', { default: false })
document.querySelector('.CodeMirror-lint-markers').style.display =
'none'
} else {
this.editor.setOption('lint', this.getCodeEditorLintConfig())
document.querySelector('.CodeMirror-lint-markers').style.display = 'inline-block'
document.querySelector('.CodeMirror-lint-markers').style.display =
'inline-block'
}
needRefresh = true
}
@@ -599,9 +627,11 @@ export default class CodeEditor extends React.Component {
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.explodingPairs !== this.props.explodingPairs) {
prevProps.explodingPairs !== this.props.explodingPairs
) {
const bracketObject = {
pairs: this.props.matchingPairs,
triples: this.props.matchingTriples,
@@ -646,11 +676,18 @@ export default class CodeEditor extends React.Component {
const elem = document.getElementById('editor-bottom-panel')
elem.parentNode.removeChild(elem)
} else {
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
this.editor.addPanel(this.createSpellCheckPanel(), {
position: 'bottom'
})
}
}
if (prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments) {
this.editor.setOption('deleteUnusedAttachments', this.props.deleteUnusedAttachments)
if (
prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments
) {
this.editor.setOption(
'deleteUnusedAttachments',
this.props.deleteUnusedAttachments
)
}
if (needRefresh) {
@@ -658,17 +695,19 @@ export default class CodeEditor extends React.Component {
}
}
getCodeEditorLintConfig () {
getCodeEditorLintConfig() {
const { mode } = this.props
const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown'
return checkMarkdownNoteIsOpen ? {
getAnnotations: this.validatorOfMarkdown,
async: true
} : false
return checkMarkdownNoteIsOpen
? {
getAnnotations: this.validatorOfMarkdown,
async: true
}
: false
}
validatorOfMarkdown (text, updateLinting) {
validatorOfMarkdown(text, updateLinting) {
const { customMarkdownLintConfig } = this.props
let lintConfigJson
try {
@@ -693,7 +732,7 @@ export default class CodeEditor extends React.Component {
let ruleNames = ''
item.ruleNames.map((ruleName, index) => {
ruleNames += ruleName
ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/'
ruleNames += index === item.ruleNames.length - 1 ? ': ' : '/'
})
const lineNumber = item.lineNumber - 1
foundIssues.push({
@@ -708,7 +747,7 @@ export default class CodeEditor extends React.Component {
})
}
setMode (mode) {
setMode(mode) {
let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text'))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
@@ -716,7 +755,7 @@ export default class CodeEditor extends React.Component {
CodeMirror.autoLoadMode(this.editor, syntax.mode)
}
handleChange (editor, changeObject) {
handleChange(editor, changeObject) {
spellcheck.handleChange(editor, changeObject)
// The current note contains an toc. We'll check for changes on headlines.
@@ -726,7 +765,11 @@ export default class CodeEditor extends React.Component {
// Check if one of the changed lines contains a headline
for (let line = 0; line < changeObject.text.length; line++) {
if (this.linePossibleContainsHeadline(editor.getLine(changeObject.from.line + line))) {
if (
this.linePossibleContainsHeadline(
editor.getLine(changeObject.from.line + line)
)
) {
requireTocUpdate = true
break
}
@@ -755,13 +798,13 @@ export default class CodeEditor extends React.Component {
}
}
linePossibleContainsHeadline (currentLine) {
linePossibleContainsHeadline(currentLine) {
// We can't check if the line start with # because when some write text before
// the # we also need to update the toc
return currentLine.includes('# ')
}
incrementLines (start, linesAdded, linesRemoved, editor) {
incrementLines(start, linesAdded, linesRemoved, editor) {
const highlightedLines = editor.options.linesHighlighted
const totalHighlightedLines = highlightedLines.length
@@ -782,7 +825,7 @@ export default class CodeEditor extends React.Component {
highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1)
// Lines that need to be relocated
if (lineNumber >= (start + linesRemoved)) {
if (lineNumber >= start + linesRemoved) {
newLines.push(lineNumber + offset)
}
}
@@ -796,22 +839,30 @@ export default class CodeEditor extends React.Component {
}
}
handleHighlight (editor, changeObject) {
handleHighlight(editor, changeObject) {
const lines = editor.options.linesHighlighted
if (!lines.includes(changeObject)) {
lines.push(changeObject)
editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
editor.addLineClass(
changeObject,
'text',
'CodeMirror-activeline-background'
)
} else {
lines.splice(lines.indexOf(changeObject), 1)
editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
editor.removeLineClass(
changeObject,
'text',
'CodeMirror-activeline-background'
)
}
if (this.props.onChange) {
this.props.onChange(editor)
}
}
updateHighlight (editor, changeObject) {
updateHighlight(editor, changeObject) {
const linesAdded = changeObject.text.length - 1
const linesRemoved = changeObject.removed.length - 1
@@ -842,28 +893,28 @@ export default class CodeEditor extends React.Component {
this.incrementLines(start, linesAdded, linesRemoved, editor)
}
moveCursorTo (row, col) {}
moveCursorTo(row, col) {}
scrollToLine (event, num) {
scrollToLine(event, num) {
const cursor = {
line: num,
ch: 1
}
this.editor.setCursor(cursor)
const top = this.editor.charCoords({line: num, ch: 0}, 'local').top
const top = this.editor.charCoords({ line: num, ch: 0 }, 'local').top
const middleHeight = this.editor.getScrollerElement().offsetHeight / 2
this.editor.scrollTo(null, top - middleHeight - 5)
}
focus () {
focus() {
this.editor.focus()
}
blur () {
blur() {
this.editor.blur()
}
reload () {
reload() {
// Change event shouldn't be fired when switch note
this.editor.off('change', this.changeHandler)
this.value = this.props.value
@@ -874,7 +925,7 @@ export default class CodeEditor extends React.Component {
this.editor.refresh()
}
setValue (value) {
setValue(value) {
const cursor = this.editor.getCursor()
this.editor.setValue(value)
this.editor.setCursor(cursor)
@@ -885,18 +936,19 @@ export default class CodeEditor extends React.Component {
* @param {Number} lineNumber
* @param {String} content
*/
setLineContent (lineNumber, content) {
setLineContent(lineNumber, content) {
const prevContent = this.editor.getLine(lineNumber)
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()
const {
storageKey,
noteKey
} = this.props
const { storageKey, noteKey } = this.props
attachmentManagement.handleAttachmentDrop(
this,
storageKey,
@@ -905,37 +957,44 @@ export default class CodeEditor extends React.Component {
)
}
insertAttachmentMd (imageMd) {
insertAttachmentMd(imageMd) {
this.editor.replaceSelection(imageMd)
}
autoDetectLanguage (content) {
autoDetectLanguage(content) {
const res = hljs.highlightAuto(content, Object.keys(languageMaps))
this.setMode(languageMaps[res.language])
}
handlePaste (editor, forceSmartPaste) {
handlePaste(editor, forceSmartPaste) {
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 startCursor = editor.getCursor('start')
const prevChar = editor.getRange({
line: startCursor.line,
ch: startCursor.ch - 2
}, {
line: startCursor.line,
ch: startCursor.ch
})
const prevChar = editor.getRange(
{
line: startCursor.line,
ch: startCursor.ch - 2
},
{
line: startCursor.line,
ch: startCursor.ch
}
)
const endCursor = editor.getCursor('end')
const nextChar = editor.getRange({
line: endCursor.line,
ch: endCursor.ch
}, {
line: endCursor.line,
ch: endCursor.ch + 1
})
const nextChar = editor.getRange(
{
line: endCursor.line,
ch: endCursor.ch
},
{
line: endCursor.line,
ch: endCursor.ch + 1
}
)
return prevChar === '](' && nextChar === ')'
}
@@ -947,7 +1006,7 @@ export default class CodeEditor extends React.Component {
return true
}
let line = line = cursor.line - 1
let line = (line = cursor.line - 1)
while (line >= 0) {
token = editor.getTokenAt({
ch: 3,
@@ -979,7 +1038,11 @@ export default class CodeEditor extends React.Component {
if (isInFencedCodeBlock(editor)) {
this.handlePasteText(editor, pastedTxt)
} else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) {
} else if (
fetchUrlTitle &&
isMarkdownTitleURL(pastedTxt) &&
!isInLinkTag(editor)
) {
this.handlePasteUrl(editor, pastedTxt)
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(editor, pastedTxt)
@@ -1015,13 +1078,13 @@ export default class CodeEditor extends React.Component {
}
}
handleScroll (e) {
handleScroll(e) {
if (this.props.onScroll) {
this.props.onScroll(e)
}
}
handlePasteUrl (editor, pastedTxt) {
handlePasteUrl(editor, pastedTxt) {
let taggedUrl = `<${pastedTxt}>`
let urlToFetch = pastedTxt
let titleMark = ''
@@ -1071,16 +1134,16 @@ export default class CodeEditor extends React.Component {
})
}
handlePasteHtml (editor, pastedHtml) {
handlePasteHtml(editor, pastedHtml) {
const markdown = this.turndownService.turndown(pastedHtml)
editor.replaceSelection(markdown)
}
handlePasteText (editor, pastedTxt) {
handlePasteText(editor, pastedTxt) {
editor.replaceSelection(pastedTxt)
}
mapNormalResponse (response, pastedTxt) {
mapNormalResponse(response, pastedTxt) {
return this.decodeResponse(response).then(body => {
return new Promise((resolve, reject) => {
try {
@@ -1088,10 +1151,12 @@ export default class CodeEditor extends React.Component {
body,
'text/html'
)
const escapePipe = (str) => {
const escapePipe = str => {
return str.replace('|', '\\|')
}
const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})`
const linkWithTitle = `[${escapePipe(
parsedBody.title
)}](${pastedTxt})`
resolve(linkWithTitle)
} catch (e) {
reject(e)
@@ -1100,7 +1165,7 @@ export default class CodeEditor extends React.Component {
})
}
initialHighlighting () {
initialHighlighting() {
if (this.editor.options.linesHighlighted == null) {
return
}
@@ -1114,16 +1179,20 @@ export default class CodeEditor extends React.Component {
// make sure that we skip the invalid lines althrough this case should not be happened.
continue
}
this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background')
this.editor.addLineClass(
lineNumber,
'text',
'CodeMirror-activeline-background'
)
}
}
restartHighlighting () {
restartHighlighting() {
this.editor.options.linesHighlighted = this.props.linesHighlighted
this.initialHighlighting()
}
mapImageResponse (response, pastedTxt) {
mapImageResponse(response, pastedTxt) {
return new Promise((resolve, reject) => {
try {
const url = response.url
@@ -1136,7 +1205,7 @@ export default class CodeEditor extends React.Component {
})
}
decodeResponse (response) {
decodeResponse(response) {
const headers = response.headers
const _charset = headers.has('content-type')
? this.extractContentTypeCharset(headers.get('content-type'))
@@ -1144,10 +1213,10 @@ export default class CodeEditor extends React.Component {
return response.arrayBuffer().then(buff => {
return new Promise((resolve, reject) => {
try {
const charset = _charset !== undefined &&
iconv.encodingExists(_charset)
? _charset
: 'utf-8'
const charset =
_charset !== undefined && iconv.encodingExists(_charset)
? _charset
: 'utf-8'
resolve(iconv.decode(Buffer.from(buff), charset).toString())
} catch (e) {
reject(e)
@@ -1156,50 +1225,49 @@ export default class CodeEditor extends React.Component {
})
}
extractContentTypeCharset (contentType) {
extractContentTypeCharset(contentType) {
return contentType
.split(';')
.filter(str => {
return str.trim().toLowerCase().startsWith('charset')
return str
.trim()
.toLowerCase()
.startsWith('charset')
})
.map(str => {
return str.replace(/['"]/g, '').split('=')[1]
})[0]
}
render () {
const {
className,
fontSize
} = this.props
render() {
const { className, fontSize } = this.props
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
const width = this.props.width
return (<
div className={
className == null ? 'CodeEditor' : `CodeEditor ${className}`
}
ref='root'
tabIndex='-1'
style={{
fontFamily,
fontSize: fontSize,
width: width
}}
onDrop={
e => this.handleDropImage(e)
}
return (
<div
className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
ref='root'
tabIndex='-1'
style={{
fontFamily,
fontSize: fontSize,
width: width
}}
onDrop={e => this.handleDropImage(e)}
/>
)
}
createSpellCheckPanel () {
createSpellCheckPanel() {
const panel = document.createElement('div')
panel.className = 'panel bottom'
panel.id = 'editor-bottom-panel'
const dropdown = document.createElement('select')
dropdown.title = 'Spellcheck'
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()
for (const op of options) {
const option = document.createElement('option')

View File

@@ -7,7 +7,7 @@ import styles from './ColorPicker.styl'
const componentHeight = 330
class ColorPicker extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -18,21 +18,21 @@ class ColorPicker extends React.Component {
this.handleConfirm = this.handleConfirm.bind(this)
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
this.onColorChange(nextProps.color)
}
onColorChange (color) {
onColorChange(color) {
this.setState({
color
})
}
handleConfirm () {
handleConfirm() {
this.props.onConfirm(this.state.color)
}
render () {
render() {
const { onReset, onCancel, targetRect } = this.props
const { color } = this.state
@@ -44,13 +44,22 @@ class ColorPicker extends React.Component {
}
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} />
<SketchPicker color={color} onChange={this.onColorChange} />
<div styleName='footer'>
<button styleName='btn-reset' onClick={onReset}>Reset</button>
<button styleName='btn-cancel' onClick={onCancel}>Cancel</button>
<button styleName='btn-confirm' onClick={this.handleConfirm}>Confirm</button>
<button styleName='btn-reset' onClick={onReset}>
Reset
</button>
<button styleName='btn-cancel' onClick={onCancel}>
Cancel
</button>
<button styleName='btn-confirm' onClick={this.handleConfirm}>
Confirm
</button>
</div>
</div>
)

View File

@@ -10,7 +10,7 @@ import ConfigManager from 'browser/main/lib/ConfigManager'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
class MarkdownEditor extends React.Component {
constructor (props) {
constructor(props) {
super(props)
// char codes for ctrl + w
@@ -20,7 +20,10 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186]
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,
keyPressed: new Set(),
isLocked: props.isLocked
@@ -29,133 +32,153 @@ class MarkdownEditor extends React.Component {
this.lockEditorCode = () => this.handleLockEditor()
}
componentDidMount () {
componentDidMount() {
this.value = this.refs.code.value
eventEmitter.on('editor:lock', this.lockEditorCode)
eventEmitter.on('editor:focus', this.focusEditor.bind(this))
}
componentDidUpdate () {
componentDidUpdate() {
this.value = this.refs.code.value
}
componentWillReceiveProps (props) {
componentWillReceiveProps(props) {
if (props.value !== this.props.value) {
this.queueRendering(props.value)
}
}
componentWillUnmount () {
componentWillUnmount() {
this.cancelQueue()
eventEmitter.off('editor:lock', this.lockEditorCode)
eventEmitter.off('editor:focus', this.focusEditor.bind(this))
}
focusEditor () {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
})
focusEditor() {
this.setState(
{
status: 'CODE'
},
() => {
this.refs.code.focus()
}
)
}
queueRendering (value) {
queueRendering(value) {
clearTimeout(this.renderTimer)
this.renderTimer = setTimeout(() => {
this.renderPreview(value)
}, 500)
}
cancelQueue () {
cancelQueue() {
clearTimeout(this.renderTimer)
}
renderPreview (value) {
renderPreview(value) {
this.setState({
renderValue: value
})
}
setValue (value) {
setValue(value) {
this.refs.code.setValue(value)
}
handleChange (e) {
handleChange(e) {
this.value = this.refs.code.value
this.props.onChange(e)
}
handleContextMenu (e) {
handleContextMenu(e) {
if (this.state.isLocked) return
const { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') {
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
this.setState({
status: newStatus
}, () => {
if (newStatus === 'CODE') {
this.refs.code.focus()
} else {
this.refs.preview.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
this.setState(
{
status: newStatus
},
() => {
if (newStatus === 'CODE') {
this.refs.code.focus()
} else {
this.refs.preview.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
const newConfig = Object.assign({}, config)
newConfig.editor.delfaultStatus = newStatus
ConfigManager.set(newConfig)
})
const newConfig = Object.assign({}, config)
newConfig.editor.delfaultStatus = newStatus
ConfigManager.set(newConfig)
}
)
}
}
handleBlur (e) {
handleBlur(e) {
if (this.state.isLocked) return
this.setState({ keyPressed: new Set() })
const { config } = this.props
if (config.editor.switchPreview === 'BLUR' ||
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
if (
config.editor.switchPreview === 'BLUR' ||
(config.editor.switchPreview === 'DBL_CLICK' &&
this.state.status === 'CODE')
) {
const cursorPosition = this.refs.code.editor.getCursor()
this.setState({
status: 'PREVIEW'
}, () => {
this.refs.preview.focus()
this.refs.preview.scrollToRow(cursorPosition.line)
})
this.setState(
{
status: 'PREVIEW'
},
() => {
this.refs.preview.focus()
this.refs.preview.scrollToRow(cursorPosition.line)
}
)
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
}
handleDoubleClick (e) {
handleDoubleClick(e) {
if (this.state.isLocked) return
this.setState({keyPressed: new Set()})
this.setState({ keyPressed: new Set() })
const { config } = this.props
if (config.editor.switchPreview === 'DBL_CLICK') {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
})
this.setState(
{
status: 'CODE'
},
() => {
this.refs.code.focus()
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
)
}
}
handlePreviewMouseDown (e) {
handlePreviewMouseDown(e) {
this.previewMouseDownedAt = new Date()
}
handlePreviewMouseUp (e) {
handlePreviewMouseUp(e) {
const { config } = this.props
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
})
if (
config.editor.switchPreview === 'BLUR' &&
new Date() - this.previewMouseDownedAt < 200
) {
this.setState(
{
status: 'CODE'
},
() => {
this.refs.code.focus()
}
)
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
}
handleCheckboxClick (e) {
handleCheckboxClick(e) {
e.preventDefault()
e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/
@@ -164,9 +187,9 @@ class MarkdownEditor extends React.Component {
const checkReplace = /\[x]/i
const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
const lines = this.refs.code.value
.split('\n')
const lineIndex =
parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
const lines = this.refs.code.value.split('\n')
const targetLine = lines[lineIndex]
let newLine = targetLine
@@ -181,45 +204,56 @@ class MarkdownEditor extends React.Component {
}
}
focus () {
focus() {
if (this.state.status === 'PREVIEW') {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
})
this.setState(
{
status: 'CODE'
},
() => {
this.refs.code.focus()
}
)
} else {
this.refs.code.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
reload () {
reload() {
this.refs.code.reload()
this.cancelQueue()
this.renderPreview(this.props.value)
}
handleKeyDown (e) {
handleKeyDown(e) {
const { config } = this.props
if (this.state.status !== 'CODE') return false
const keyPressed = this.state.keyPressed
keyPressed.add(e.keyCode)
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
if (keyPressed.size === this.escapeFromEditor.length &&
!this.state.isLocked && this.state.status === 'CODE' &&
this.escapeFromEditor.every(isNoteHandlerKey)) {
if (
keyPressed.size === this.escapeFromEditor.length &&
!this.state.isLocked &&
this.state.status === 'CODE' &&
this.escapeFromEditor.every(isNoteHandlerKey)
) {
this.handleContextMenu()
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('**')
}
}
addMdAroundWord (mdElement) {
addMdAroundWord(mdElement) {
if (this.refs.code.editor.getSelection()) {
return this.addMdAroundSelection(mdElement)
}
@@ -227,47 +261,63 @@ class MarkdownEditor extends React.Component {
const word = this.refs.code.editor.findWordAt(currentCaret)
const cmDoc = this.refs.code.editor.getDoc()
cmDoc.replaceRange(mdElement, word.anchor)
cmDoc.replaceRange(mdElement, { line: word.head.line, ch: word.head.ch + mdElement.length })
}
addMdAroundSelection (mdElement) {
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
}
handleDropImage (dropEvent) {
dropEvent.preventDefault()
const { storageKey, noteKey } = this.props
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
this.refs.code.editor.execCommand('goDocEnd')
this.refs.code.editor.execCommand('goLineEnd')
this.refs.code.editor.execCommand('newlineAndIndent')
attachmentManagement.handleAttachmentDrop(
this.refs.code,
storageKey,
noteKey,
dropEvent
)
cmDoc.replaceRange(mdElement, {
line: word.head.line,
ch: word.head.ch + mdElement.length
})
}
handleKeyUp (e) {
addMdAroundSelection(mdElement) {
this.refs.code.editor.replaceSelection(
`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`
)
}
handleDropImage(dropEvent) {
dropEvent.preventDefault()
const { storageKey, noteKey } = this.props
this.setState(
{
status: 'CODE'
},
() => {
this.refs.code.focus()
this.refs.code.editor.execCommand('goDocEnd')
this.refs.code.editor.execCommand('goLineEnd')
this.refs.code.editor.execCommand('newlineAndIndent')
attachmentManagement.handleAttachmentDrop(
this.refs.code,
storageKey,
noteKey,
dropEvent
)
}
)
}
handleKeyUp(e) {
const keyPressed = this.state.keyPressed
keyPressed.delete(e.keyCode)
this.setState({ keyPressed })
}
handleLockEditor () {
handleLockEditor() {
this.setState({ isLocked: !this.state.isLocked })
}
render () {
const {className, value, config, storageKey, noteKey, linesHighlighted, RTL} = this.props
render() {
const {
className,
value,
config,
storageKey,
noteKey,
linesHighlighted,
RTL
} = this.props
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -275,23 +325,24 @@ class MarkdownEditor extends React.Component {
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
const previewStyle = {}
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
if (this.props.ignorePreviewPointerEvents)
previewStyle.pointerEvents = 'none'
const storage = findStorage(storageKey)
return (
<div className={className == null
? 'MarkdownEditor'
: `MarkdownEditor ${className}`
<div
className={
className == null ? 'MarkdownEditor' : `MarkdownEditor ${className}`
}
onContextMenu={(e) => this.handleContextMenu(e)}
onContextMenu={e => this.handleContextMenu(e)}
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
onKeyUp={(e) => this.handleKeyUp(e)}
onKeyDown={e => this.handleKeyDown(e)}
onKeyUp={e => this.handleKeyUp(e)}
>
<CodeEditor styleName={this.state.status === 'CODE'
? 'codeEditor'
: 'codeEditor--hide'
<CodeEditor
styleName={
this.state.status === 'CODE' ? 'codeEditor' : 'codeEditor--hide'
}
ref='code'
mode='Boost Flavored Markdown'
@@ -315,8 +366,8 @@ class MarkdownEditor extends React.Component {
fetchUrlTitle={config.editor.fetchUrlTitle}
enableTableEditor={config.editor.enableTableEditor}
linesHighlighted={linesHighlighted}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
onChange={e => this.handleChange(e)}
onBlur={e => this.handleBlur(e)}
spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
@@ -327,9 +378,9 @@ class MarkdownEditor extends React.Component {
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL}
/>
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview'
: 'preview--hide'
<MarkdownPreview
styleName={
this.state.status === 'PREVIEW' ? 'preview' : 'preview--hide'
}
style={previewStyle}
theme={config.ui.theme}
@@ -347,20 +398,20 @@ class MarkdownEditor extends React.Component {
sanitize={config.preview.sanitize}
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)}
onDoubleClick={(e) => this.handleDoubleClick(e)}
onContextMenu={e => this.handleContextMenu(e)}
onDoubleClick={e => this.handleDoubleClick(e)}
tabIndex='0'
value={this.state.renderValue}
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
onMouseUp={e => this.handlePreviewMouseUp(e)}
onMouseDown={e => this.handlePreviewMouseDown(e)}
onCheckboxClick={e => this.handleCheckboxClick(e)}
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path}
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
onDrop={(e) => this.handleDropImage(e)}
onDrop={e => this.handleDropImage(e)}
RTL={RTL}
/>
</div>

View File

@@ -26,7 +26,8 @@ import i18n from 'browser/lib/i18n'
const { remote, shell } = require('electron')
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 path = require('path')
@@ -56,7 +57,7 @@ const CSS_FILES = [
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
* @returns {String}
*/
function buildStyle (opts) {
function buildStyle(opts) {
const {
fontFamily,
fontSize,
@@ -104,11 +105,14 @@ body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
${scrollPastEnd ? `
${
scrollPastEnd
? `
padding-bottom: 90vh;
box-sizing: border-box;
`
: ''}
: ''
}
${RTL ? 'direction: rtl;' : ''}
${RTL ? 'text-align: right;' : ''}
}
@@ -225,7 +229,7 @@ const defaultCodeBlockFontFamily = [
// return the line number of the line that used to generate the specified element
// return -1 if the line is not found
function getSourceLineNumberByElement (element) {
function getSourceLineNumberByElement(element) {
let isHasLineNumber = element.dataset.line !== undefined
let parent = element
while (!isHasLineNumber && parent.parentElement !== null) {
@@ -236,7 +240,7 @@ function getSourceLineNumberByElement (element) {
}
export default class MarkdownPreview extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.contextMenuHandler = e => this.handleContextMenu(e)
@@ -260,7 +264,7 @@ export default class MarkdownPreview extends React.Component {
this.initMarkdown()
}
initMarkdown () {
initMarkdown() {
const { smartQuotes, sanitize, breaks } = this.props
this.markdown = new Markdown({
typographer: smartQuotes,
@@ -269,17 +273,17 @@ export default class MarkdownPreview extends React.Component {
})
}
handleCheckboxClick (e) {
handleCheckboxClick(e) {
this.props.onCheckboxClick(e)
}
handleScroll (e) {
handleScroll(e) {
if (this.props.onScroll) {
this.props.onScroll(e)
}
}
handleContextMenu (event) {
handleContextMenu(event) {
const menu = buildMarkdownPreviewContextMenu(this, event)
const switchPreview = ConfigManager.get().editor.switchPreview
if (menu != null && switchPreview !== 'RIGHTCLICK') {
@@ -289,17 +293,21 @@ export default class MarkdownPreview extends React.Component {
}
}
handleDoubleClick (e) {
handleDoubleClick(e) {
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
}
handleMouseDown (e) {
handleMouseDown(e) {
const config = ConfigManager.get()
const clickElement = e.target
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV"
const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
if (
config.editor.switchPreview === 'RIGHTCLICK' &&
e.buttons === 2 &&
config.editor.type === 'SPLIT'
) {
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
}
if (e.ctrlKey) {
@@ -315,10 +323,11 @@ 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) {
if (!this.props.onMouseUp) return
if (e.target != null && e.target.tagName === 'A') {
return null
@@ -326,15 +335,15 @@ export default class MarkdownPreview extends React.Component {
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
}
handleSaveAsText () {
handleSaveAsText() {
this.exportAsDocument('txt')
}
handleSaveAsMd () {
handleSaveAsMd() {
this.exportAsDocument('md')
}
htmlContentFormatter (noteContent, exportTasks, targetDir) {
htmlContentFormatter(noteContent, exportTasks, targetDir) {
const {
fontFamily,
fontSize,
@@ -360,10 +369,7 @@ export default class MarkdownPreview extends React.Component {
RTL
})
let body = this.refs.root.contentWindow.document.body.innerHTML
body = attachmentManagement.fixLocalURLS(
body,
this.props.storagePath
)
body = attachmentManagement.fixLocalURLS(body, this.props.storagePath)
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach(file => {
if (global.process.platform === 'win32') {
@@ -394,14 +400,24 @@ export default class MarkdownPreview extends React.Component {
</html>`
}
handleSaveAsHtml () {
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir)))
handleSaveAsHtml() {
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) =>
Promise.resolve(
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
)
)
}
handleSaveAsPdf () {
handleSaveAsPdf() {
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false, javascript: false}})
printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir))
const printout = new remote.BrowserWindow({
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) => {
printout.webContents.on('did-finish-load', () => {
printout.webContents.printToPDF({}, (err, data) => {
@@ -414,11 +430,11 @@ export default class MarkdownPreview extends React.Component {
})
}
handlePrint () {
handlePrint() {
this.refs.root.contentWindow.print()
}
exportAsDocument (fileType, contentFormatter) {
exportAsDocument(fileType, contentFormatter) {
const options = {
filters: [{ name: 'Documents', extensions: [fileType] }],
properties: ['openFile', 'createDirectory']
@@ -449,7 +465,7 @@ export default class MarkdownPreview extends React.Component {
})
}
fixDecodedURI (node) {
fixDecodedURI(node) {
if (
node &&
node.children.length === 1 &&
@@ -466,17 +482,18 @@ export default class MarkdownPreview extends React.Component {
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
* @returns {string} HTML in which special characters between three ``` have been converted
*/
escapeHtmlCharactersInCodeTag (splitWithCodeTag) {
escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
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) {
splitWithCodeTag.splice((index + 1), 0, '\`\`\`')
splitWithCodeTag.splice(index + 1, 0, '```')
}
}
let inCodeTag = false
let result = ''
for (let content of splitWithCodeTag) {
if (content === '\`\`\`') {
if (content === '```') {
inCodeTag = !inCodeTag
} else if (inCodeTag) {
content = escapeHtmlCharacters(content)
@@ -486,13 +503,15 @@ export default class MarkdownPreview extends React.Component {
return result
}
getScrollBarStyle () {
getScrollBarStyle() {
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() {
const { onDrop } = this.props
this.refs.root.setAttribute('sandbox', 'allow-scripts')
@@ -542,10 +561,7 @@ export default class MarkdownPreview extends React.Component {
'scroll',
this.scrollHandler
)
this.refs.root.contentWindow.addEventListener(
'resize',
this.resizeHandler
)
this.refs.root.contentWindow.addEventListener('resize', this.resizeHandler)
eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
@@ -553,7 +569,7 @@ export default class MarkdownPreview extends React.Component {
eventEmitter.on('print', this.printHandler)
}
componentWillUnmount () {
componentWillUnmount() {
const { onDrop } = this.props
this.refs.root.contentWindow.document.body.removeEventListener(
@@ -595,7 +611,7 @@ export default class MarkdownPreview extends React.Component {
eventEmitter.off('print', this.printHandler)
}
componentDidUpdate (prevProps) {
componentDidUpdate(prevProps) {
// actual rewriteIframe function should be called only once
let needsRewriteIframe = false
if (prevProps.value !== this.props.value) needsRewriteIframe = true
@@ -637,7 +653,7 @@ export default class MarkdownPreview extends React.Component {
}
}
getStyleParams () {
getStyleParams() {
const {
fontSize,
lineNumber,
@@ -649,19 +665,20 @@ export default class MarkdownPreview extends React.Component {
RTL
} = this.props
let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
fontFamily =
_.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily =
_.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
return {
fontFamily,
@@ -677,7 +694,7 @@ export default class MarkdownPreview extends React.Component {
}
}
applyStyle () {
applyStyle() {
const {
fontFamily,
fontSize,
@@ -707,7 +724,7 @@ export default class MarkdownPreview extends React.Component {
})
}
getCodeThemeLink (name) {
getCodeThemeLink(name) {
const theme = consts.THEMES.find(theme => theme.name === name)
return theme != null
@@ -715,7 +732,7 @@ export default class MarkdownPreview extends React.Component {
: `${appPath}/node_modules/codemirror/theme/elegant.css`
}
rewriteIframe () {
rewriteIframe() {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll(
'input[type="checkbox"]'
@@ -773,7 +790,9 @@ export default class MarkdownPreview extends React.Component {
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(
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
@@ -859,7 +878,10 @@ export default class MarkdownPreview extends React.Component {
el => {
try {
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 = ''
const canvas = document.createElement('canvas')
@@ -882,7 +904,12 @@ export default class MarkdownPreview extends React.Component {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
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
}
render(
<Carousel
images={images}
autoplay={autoplay}
/>,
el
)
render(<Carousel images={images} autoplay={autoplay} />, el)
}
)
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
const rect = markdownPreviewIframe.getBoundingClientRect()
const config = { attributes: true, subtree: true }
const imgObserver = new MutationObserver((mutationList) => {
const imgObserver = new MutationObserver(mutationList => {
for (const mu of mutationList) {
if (mu.target.className === 'carouselContent-enter-done') {
this.setImgOnClickEventHelper(mu.target, rect)
@@ -926,26 +947,32 @@ 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) {
const parentEl = img.parentElement
this.setImgOnClickEventHelper(img, rect)
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) {
a.removeEventListener('click', this.linkClickHandler)
a.addEventListener('click', this.linkClickHandler)
}
}
setImgOnClickEventHelper (img, rect) {
setImgOnClickEventHelper(img, rect) {
img.onclick = () => {
const widthMagnification = document.body.clientWidth / img.width
const heightMagnification = document.body.clientHeight / img.height
const baseOnWidth = widthMagnification < heightMagnification
const magnification = baseOnWidth ? widthMagnification : heightMagnification
const magnification = baseOnWidth
? widthMagnification
: heightMagnification
const zoomImgWidth = img.width * magnification
const zoomImgHeight = img.height * magnification
@@ -976,10 +1003,7 @@ export default class MarkdownPreview extends React.Component {
width: ${zoomImgWidth};
height: ${zoomImgHeight}px;
`
zoomImg.animate([
originalImgRect,
zoomInImgRect
], animationSpeed)
zoomImg.animate([originalImgRect, zoomInImgRect], animationSpeed)
const overlay = document.createElement('div')
overlay.style = `
@@ -1000,10 +1024,10 @@ export default class MarkdownPreview extends React.Component {
width: ${img.width}px;
height: ${img.height}px;
`
const zoomOutImgAnimation = zoomImg.animate([
zoomInImgRect,
originalImgRect
], animationSpeed)
const zoomOutImgAnimation = zoomImg.animate(
[zoomInImgRect, originalImgRect],
animationSpeed
)
zoomOutImgAnimation.onfinish = () => overlay.remove()
}
@@ -1012,7 +1036,7 @@ export default class MarkdownPreview extends React.Component {
}
}
handleResize () {
handleResize() {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('svg[ratio]'),
el => {
@@ -1021,11 +1045,11 @@ export default class MarkdownPreview extends React.Component {
)
}
focus () {
focus() {
this.refs.root.focus()
}
getWindow () {
getWindow() {
return this.refs.root.contentWindow
}
@@ -1033,7 +1057,7 @@ export default class MarkdownPreview extends React.Component {
* @public
* @param {Number} targetRow
*/
scrollToRow (targetRow) {
scrollToRow(targetRow) {
const blocks = this.getWindow().document.querySelectorAll(
'body>[data-line]'
)
@@ -1054,16 +1078,16 @@ export default class MarkdownPreview extends React.Component {
* @param {Number} x
* @param {Number} y
*/
scrollTo (x, y) {
scrollTo(x, y) {
this.getWindow().document.body.scrollTo(x, y)
}
preventImageDroppedHandler (e) {
preventImageDroppedHandler(e) {
e.preventDefault()
e.stopPropagation()
}
notify (title, options) {
notify(title, options) {
if (global.process.platform === 'win32') {
options.icon = path.join(
'file://',
@@ -1074,7 +1098,7 @@ export default class MarkdownPreview extends React.Component {
return new window.Notification(title, options)
}
handleLinkClick (e) {
handleLinkClick(e) {
e.preventDefault()
e.stopPropagation()
@@ -1095,9 +1119,7 @@ export default class MarkdownPreview extends React.Component {
if (posOfHash > -1) {
const extractedId = linkHash.slice(posOfHash + 1)
const targetId = mdurl.encode(extractedId)
const targetElement = this.getWindow().document.getElementById(
targetId
)
const targetElement = this.getWindow().document.getElementById(targetId)
if (targetElement != null) {
this.scrollTo(0, targetElement.offsetTop)
@@ -1138,9 +1160,10 @@ export default class MarkdownPreview extends React.Component {
this.openExternal(href)
}
openExternal (href) {
openExternal(href) {
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)
} catch (e) {
// URI Error threw from decodeURI
@@ -1148,7 +1171,7 @@ export default class MarkdownPreview extends React.Component {
}
}
render () {
render() {
const { className, style, tabIndex } = this.props
return (
<iframe

View File

@@ -8,7 +8,7 @@ import styles from './MarkdownSplitEditor.styl'
import CSSModules from 'browser/lib/CSSModules'
class MarkdownSplitEditor extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.value = props.value
this.focus = () => this.refs.code.focus()
@@ -20,19 +20,22 @@ class MarkdownSplitEditor extends React.Component {
}
}
setValue (value) {
setValue(value) {
this.refs.code.setValue(value)
}
handleOnChange (e) {
handleOnChange(e) {
this.value = this.refs.code.value
this.props.onChange(e)
}
handleScroll (e) {
handleScroll(e) {
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')
let srcTop, srcHeight, targetTop, targetHeight
@@ -49,7 +52,7 @@ class MarkdownSplitEditor extends React.Component {
targetHeight = _.get(codeDoc, 'height')
}
const distance = (targetHeight * srcTop / srcHeight) - targetTop
const distance = (targetHeight * srcTop) / srcHeight - targetTop
const framerate = 1000 / 60
const frames = 20
const refractory = frames * framerate
@@ -60,21 +63,29 @@ class MarkdownSplitEditor extends React.Component {
let scrollPos, time
const timer = setInterval(() => {
time = frame / frames
scrollPos = time < 0.5
? 2 * time * time // ease in
: -1 + (4 - 2 * time) * time // ease out
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance)
scrollPos =
time < 0.5
? 2 * time * time // ease in
: -1 + (4 - 2 * time) * time // ease out
if (e.doc)
_.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
else
_.get(this, 'refs.code.editor').scrollTo(
0,
targetTop + scrollPos * distance
)
if (frame >= frames) {
clearInterval(timer)
setTimeout(() => { this.userScroll = true }, refractory)
setTimeout(() => {
this.userScroll = true
}, refractory)
}
frame++
}, framerate)
}
}
handleCheckboxClick (e) {
handleCheckboxClick(e) {
e.preventDefault()
e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/
@@ -83,9 +94,9 @@ class MarkdownSplitEditor extends React.Component {
const checkReplace = /\[x]/i
const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
const lines = this.refs.code.value
.split('\n')
const lineIndex =
parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
const lines = this.refs.code.value.split('\n')
const targetLine = lines[lineIndex]
let newLine = targetLine
@@ -100,12 +111,12 @@ class MarkdownSplitEditor extends React.Component {
}
}
handleMouseMove (e) {
handleMouseMove(e) {
if (this.state.isSliderFocused) {
const rootRect = this.refs.root.getBoundingClientRect()
const rootWidth = rootRect.width
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%
if (newCodeEditorWidthInPercent <= 10) {
@@ -122,34 +133,45 @@ class MarkdownSplitEditor extends React.Component {
}
}
handleMouseUp (e) {
handleMouseUp(e) {
e.preventDefault()
this.setState({
isSliderFocused: false
})
}
handleMouseDown (e) {
handleMouseDown(e) {
e.preventDefault()
this.setState({
isSliderFocused: true
})
}
render () {
const {config, value, storageKey, noteKey, linesHighlighted, RTL} = this.props
render() {
const {
config,
value,
storageKey,
noteKey,
linesHighlighted,
RTL
} = this.props
const storage = findStorage(storageKey)
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
const previewStyle = {}
previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%'
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
previewStyle.width = 100 - this.state.codeEditorWidthInPercent + '%'
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused)
previewStyle.pointerEvents = 'none'
return (
<div styleName='root' ref='root'
<div
styleName='root'
ref='root'
onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}>
onMouseUp={e => this.handleMouseUp(e)}
>
<CodeEditor
ref='code'
width={this.state.codeEditorWidthInPercent + '%'}
@@ -174,7 +196,7 @@ class MarkdownSplitEditor extends React.Component {
storageKey={storageKey}
noteKey={noteKey}
linesHighlighted={linesHighlighted}
onChange={(e) => this.handleOnChange(e)}
onChange={e => this.handleOnChange(e)}
onScroll={this.handleScroll.bind(this)}
spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste}
@@ -184,8 +206,12 @@ class MarkdownSplitEditor extends React.Component {
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
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>
<MarkdownPreview
@@ -206,7 +232,7 @@ class MarkdownSplitEditor extends React.Component {
ref='preview'
tabInde='0'
value={value}
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
onCheckboxClick={e => this.handleCheckboxClick(e)}
onScroll={this.handleScroll.bind(this)}
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path}
@@ -215,7 +241,7 @@ class MarkdownSplitEditor extends React.Component {
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
RTL={RTL}
/>
/>
</div>
)
}

View File

@@ -3,9 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ModalEscButton.styl'
const ModalEscButton = ({
handleEscButtonClick
}) => (
const ModalEscButton = ({ handleEscButtonClick }) => (
<button styleName='escButton' onClick={handleEscButtonClick}>
<div styleName='esc-mark'>×</div>
<div>esc</div>

View File

@@ -1,24 +1,23 @@
/**
* @fileoverview Micro component for toggle SideNav
*/
* @fileoverview Micro component for toggle SideNav
*/
import PropTypes from 'prop-types'
import React from 'react'
import styles from './NavToggleButton.styl'
import CSSModules from 'browser/lib/CSSModules'
/**
* @param {boolean} isFolded
* @param {Function} handleToggleButtonClick
*/
* @param {boolean} isFolded
* @param {Function} handleToggleButtonClick
*/
const NavToggleButton = ({isFolded, handleToggleButtonClick}) => (
<button styleName='navToggle'
onClick={(e) => handleToggleButtonClick(e)}
>
{isFolded
? <i className='fa fa-angle-double-right fa-2x' />
: <i className='fa fa-angle-double-left fa-2x' />
}
const NavToggleButton = ({ isFolded, handleToggleButtonClick }) => (
<button styleName='navToggle' onClick={e => handleToggleButtonClick(e)}>
{isFolded ? (
<i className='fa fa-angle-double-right fa-2x' />
) : (
<i className='fa fa-angle-double-left fa-2x' />
)}
</button>
)

View File

@@ -22,7 +22,11 @@ const TagElement = ({ tagName, color }) => {
const style = {}
if (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 (
<span styleName='item-bottom-tagList-item' key={tagName} style={style}>
@@ -44,9 +48,13 @@ const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
}
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 {
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'
>
<div styleName='item-wrapper'>
{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-file-text-o' />}
{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-file-text-o' />
)}
<div styleName='item-title'>
{note.title.trim().length > 0
? <Emoji text={note.title} />
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
{note.title.trim().length > 0 ? (
<Emoji text={note.title} />
) : (
<span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
)}
</div>
<div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div>
@@ -98,7 +110,9 @@ const NoteItem = ({
title={
viewType === 'ALL'
? storageName
: viewType === 'STORAGE' ? folderName : null
: viewType === 'STORAGE'
? folderName
: null
}
styleName='item-middle-app-meta-label'
>
@@ -109,28 +123,36 @@ const NoteItem = ({
</div>
<div styleName='item-bottom'>
<div styleName='item-bottom-tagList'>
{note.tags.length > 0
? TagElementList(note.tags, showTagsAlphabetically, coloredTags)
: <span
{note.tags.length > 0 ? (
TagElementList(note.tags, showTagsAlphabetically, coloredTags)
) : (
<span
style={{ fontStyle: 'italic', opacity: 0.5 }}
styleName='item-bottom-tagList-empty'
>
>
{i18n.__('No tags')}
</span>}
</span>
)}
</div>
<div>
{note.isStarred
? <img
{note.isStarred ? (
<img
styleName='item-star'
src='../resources/icon/icon-starred.svg'
/>
: ''}
{note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' />
: ''}
{note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
: ''}
/>
) : (
''
)}
{note.isPinned && !pathname.match(/\/starred|\/trash/) ? (
<i styleName='item-pin' className='fa fa-thumb-tack' />
) : (
''
)}
{note.type === 'MARKDOWN_NOTE' ? (
<TodoProcess todoStatus={getTodoStatus(note.content)} />
) : (
''
)}
</div>
</div>
</div>

View File

@@ -25,10 +25,8 @@ const NoteItemSimple = ({
pathname,
storage
}) => (
<div styleName={isActive
? 'item-simple--active'
: 'item-simple'
}
<div
styleName={isActive ? 'item-simple--active' : 'item-simple'}
key={note.key}
onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)}
@@ -36,23 +34,29 @@ const NoteItemSimple = ({
draggable='true'
>
<div styleName='item-simple-title'>
{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-file-text-o' />
}
{note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' />
: ''
}
{note.title.trim().length > 0
? note.title
: <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>}
{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-file-text-o'
/>
)}
{note.isPinned && !pathname.match(/\/starred|\/trash/) ? (
<i styleName='item-pin' className='fa fa-thumb-tack' />
) : (
''
)}
{note.title.trim().length > 0 ? (
note.title
) : (
<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>
)

View File

@@ -6,7 +6,7 @@ const electron = require('electron')
const { shell } = electron
class RealtimeNotification extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -14,38 +14,46 @@ class RealtimeNotification extends React.Component {
}
}
componentDidMount () {
componentDidMount() {
this.fetchNotifications()
}
fetchNotifications () {
const notificationsUrl = 'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
fetchNotifications() {
const notificationsUrl =
'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
fetch(notificationsUrl)
.then(response => {
return response.json()
})
.then(json => {
this.setState({notifications: json.notifications})
this.setState({ notifications: json.notifications })
})
}
handleLinkClick (e) {
handleLinkClick(e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
render () {
render() {
const { notifications } = this.state
const link = notifications.length > 0
? <a styleName='notification-link' href={notifications[0].linkUrl}
onClick={(e) => this.handleLinkClick(e)}
>
Info: {notifications[0].text}
</a>
: ''
const link =
notifications.length > 0 ? (
<a
styleName='notification-link'
href={notifications[0].linkUrl}
onClick={e => this.handleLinkClick(e)}
>
Info: {notifications[0].text}
</a>
) : (
''
)
return (
<div styleName='notification-area' style={this.props.style}>{link}</div>
<div styleName='notification-area' style={this.props.style}>
{link}
</div>
)
}
}

View File

@@ -16,54 +16,70 @@ import i18n from 'browser/lib/i18n'
* @return {React.Component}
*/
const SideNavFilter = ({
isFolded, isHomeActive, handleAllNotesButtonClick,
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
isFolded,
isHomeActive,
handleAllNotesButtonClick,
isStarredActive,
handleStarredButtonClick,
isTrashedActive,
handleTrashedButtonClick,
counterDelNote,
counterTotalNote,
counterStarredNote,
handleFilterButtonContextMenu
}) => (
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
<button
styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
onClick={handleAllNotesButtonClick}
>
<div styleName='iconWrap'>
<img src={isHomeActive
? '../resources/icon/icon-all-active.svg'
: '../resources/icon/icon-all.svg'
}
<img
src={
isHomeActive
? '../resources/icon/icon-all-active.svg'
: '../resources/icon/icon-all.svg'
}
/>
</div>
<span styleName='menu-button-label'>{i18n.__('All Notes')}</span>
<span styleName='counters'>{counterTotalNote}</span>
</button>
<button styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'}
<button
styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'}
onClick={handleStarredButtonClick}
>
<div styleName='iconWrap'>
<img src={isStarredActive
? '../resources/icon/icon-star-active.svg'
: '../resources/icon/icon-star-sidenav.svg'
}
<img
src={
isStarredActive
? '../resources/icon/icon-star-active.svg'
: '../resources/icon/icon-star-sidenav.svg'
}
/>
</div>
<span styleName='menu-button-label'>{i18n.__('Starred')}</span>
<span styleName='counters'>{counterStarredNote}</span>
</button>
<button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
onClick={handleTrashedButtonClick} onContextMenu={handleFilterButtonContextMenu}
<button
styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
onClick={handleTrashedButtonClick}
onContextMenu={handleFilterButtonContextMenu}
>
<div styleName='iconWrap'>
<img src={isTrashedActive
? '../resources/icon/icon-trash-active.svg'
: '../resources/icon/icon-trash-sidenav.svg'
}
<img
src={
isTrashedActive
? '../resources/icon/icon-trash-active.svg'
: '../resources/icon/icon-trash-sidenav.svg'
}
/>
</div>
<span styleName='menu-button-label'>{i18n.__('Trash')}</span>
<span styleName='counters'>{counterDelNote}</span>
</button>
</div>
)

View File

@@ -5,7 +5,7 @@ import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
class SnippetTab extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -14,7 +14,7 @@ class SnippetTab extends React.Component {
}
}
componentWillUpdate (nextProps) {
componentWillUpdate(nextProps) {
if (nextProps.snippet.name !== this.props.snippet.name) {
this.setState({
name: nextProps.snippet.name
@@ -22,34 +22,34 @@ class SnippetTab extends React.Component {
}
}
handleClick (e) {
handleClick(e) {
this.props.onClick(e)
}
handleContextMenu (e) {
handleContextMenu(e) {
context.popup([
{
label: i18n.__('Rename'),
click: (e) => this.handleRenameClick(e)
click: e => this.handleRenameClick(e)
}
])
}
handleRenameClick (e) {
handleRenameClick(e) {
this.startRenaming()
}
handleNameInputBlur (e) {
handleNameInputBlur(e) {
this.handleRename()
}
handleNameInputChange (e) {
handleNameInputChange(e) {
this.setState({
name: e.target.value
})
}
handleNameInputKeyDown (e) {
handleNameInputKeyDown(e) {
switch (e.keyCode) {
case 13:
this.handleRename()
@@ -63,84 +63,87 @@ class SnippetTab extends React.Component {
}
}
handleRename () {
this.setState({
isRenaming: false
}, () => {
if (this.props.snippet.name !== this.state.name) {
this.props.onRename(this.state.name)
handleRename() {
this.setState(
{
isRenaming: false
},
() => {
if (this.props.snippet.name !== this.state.name) {
this.props.onRename(this.state.name)
}
}
})
)
}
handleDeleteButtonClick (e) {
handleDeleteButtonClick(e) {
this.props.onDelete(e)
}
startRenaming () {
this.setState({
isRenaming: true
}, () => {
this.refs.name.focus()
this.refs.name.select()
})
startRenaming() {
this.setState(
{
isRenaming: true
},
() => {
this.refs.name.focus()
this.refs.name.select()
}
)
}
handleDragStart (e) {
handleDragStart(e) {
e.dataTransfer.dropEffect = 'move'
this.props.onDragStart(e)
}
handleDrop (e) {
handleDrop(e) {
this.props.onDrop(e)
}
render () {
render() {
const { isActive, snippet, isDeletable } = this.props
return (
<div styleName={isActive
? 'root--active'
: 'root'
}
>
{!this.state.isRenaming
? <button styleName='button'
onClick={(e) => this.handleClick(e)}
onDoubleClick={(e) => this.handleRenameClick(e)}
onContextMenu={(e) => this.handleContextMenu(e)}
onDragStart={(e) => this.handleDragStart(e)}
onDrop={(e) => this.handleDrop(e)}
<div styleName={isActive ? 'root--active' : 'root'}>
{!this.state.isRenaming ? (
<button
styleName='button'
onClick={e => this.handleClick(e)}
onDoubleClick={e => this.handleRenameClick(e)}
onContextMenu={e => this.handleContextMenu(e)}
onDragStart={e => this.handleDragStart(e)}
onDrop={e => this.handleDrop(e)}
draggable='true'
>
{snippet.name.trim().length > 0
? snippet.name
: <span>
{i18n.__('Unnamed')}
</span>
}
{snippet.name.trim().length > 0 ? (
snippet.name
) : (
<span>{i18n.__('Unnamed')}</span>
)}
</button>
: <input styleName='input'
) : (
<input
styleName='input'
ref='name'
value={this.state.name}
onChange={(e) => this.handleNameInputChange(e)}
onBlur={(e) => this.handleNameInputBlur(e)}
onKeyDown={(e) => this.handleNameInputKeyDown(e)}
onChange={e => this.handleNameInputChange(e)}
onBlur={e => this.handleNameInputBlur(e)}
onKeyDown={e => this.handleNameInputKeyDown(e)}
/>
}
{isDeletable &&
<button styleName='deleteButton'
onClick={(e) => this.handleDeleteButtonClick(e)}
)}
{isDeletable && (
<button
styleName='deleteButton'
onClick={e => this.handleDeleteButtonClick(e)}
>
<i className='fa fa-times' />
</button>
}
)}
</div>
)
}
}
SnippetTab.propTypes = {
}
SnippetTab.propTypes = {}
export default CSSModules(SnippetTab, styles)

View File

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

View File

@@ -1,18 +1,20 @@
/**
* @fileoverview Micro component for showing StorageList
*/
* @fileoverview Micro component for showing StorageList
*/
import PropTypes from 'prop-types'
import React from 'react'
import styles from './StorageList.styl'
import CSSModules from 'browser/lib/CSSModules'
/**
* @param {Array} storageList
*/
* @param {Array} storageList
*/
const StorageList = ({storageList, isFolded}) => (
const StorageList = ({ storageList, isFolded }) => (
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
{storageList.length > 0 ? storageList : (
{storageList.length > 0 ? (
storageList
) : (
<div styleName='storageList-empty'>No storage mount.</div>
)}
</div>

View File

@@ -1,30 +1,58 @@
/**
* @fileoverview Micro component for showing TagList.
*/
* @fileoverview Micro component for showing TagList.
*/
import PropTypes from 'prop-types'
import React from 'react'
import styles from './TagListItem.styl'
import CSSModules from 'browser/lib/CSSModules'
/**
* @param {string} name
* @param {Function} handleClickTagListItem
* @param {Function} handleClickNarrowToTag
* @param {boolean} isActive
* @param {boolean} isRelated
* @param {string} bgColor tab backgroundColor
*/
* @param {string} name
* @param {Function} handleClickTagListItem
* @param {Function} handleClickNarrowToTag
* @param {boolean} isActive
* @param {boolean} isRelated
* @param {string} bgColor tab backgroundColor
*/
const TagListItem = ({name, handleClickTagListItem, 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)}>
const TagListItem = ({
name,
handleClickTagListItem,
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'} />
</button>
: <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'}} />
) : (
<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' }}
/>
<span styleName='tagList-item-name'>
{`# ${name}`}
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>

View File

@@ -11,17 +11,20 @@ import styles from './TodoListPercentage.styl'
* @param {number} percentageOfTodo
*/
const TodoListPercentage = ({
percentageOfTodo, onClearCheckboxClick
}) => (
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
const TodoListPercentage = ({ percentageOfTodo, onClearCheckboxClick }) => (
<div
styleName='percentageBar'
style={{ display: isNaN(percentageOfTodo) ? 'none' : '' }}
>
<div styleName='progressBar' style={{ width: `${percentageOfTodo}%` }}>
<div styleName='progressBarInner'>
<p styleName='percentageText'>{percentageOfTodo}%</p>
</div>
</div>
<div styleName='todoClear'>
<p styleName='todoClearText' onClick={(e) => onClearCheckboxClick(e)}>clear</p>
<p styleName='todoClearText' onClick={e => onClearCheckboxClick(e)}>
clear
</p>
</div>
</div>
)

View File

@@ -8,18 +8,21 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TodoProcess.styl'
const TodoProcess = ({
todoStatus: {
total: totalTodo,
completed: completedTodo
}
todoStatus: { 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'>
<i className='fa fa-fw fa-check-square-o' />
{completedTodo} of {totalTodo}
</div>
<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>
)

View File

@@ -7,11 +7,11 @@ const darkThemeStyling = `
fill: white;
}`
function getRandomInt (min, max) {
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min
}
function getId () {
function getId() {
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let id = 'm-'
for (let i = 0; i < 7; i++) {
@@ -20,7 +20,7 @@ function getId () {
return id
}
function render (element, content, theme, enableHTMLLabel) {
function render(element, content, theme, enableHTMLLabel) {
try {
const height = element.attributes.getNamedItem('data-height')
const isPredefined = height && height.value !== 'undefined'
@@ -29,7 +29,9 @@ function render (element, content, theme, enableHTMLLabel) {
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({
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
if (!isPredefined) {

View File

@@ -1,5 +1,5 @@
import CSSModules from 'react-css-modules'
export default function (component, styles) {
return CSSModules(component, styles, {handleNotFoundStyleName: 'log'})
export default function(component, styles) {
return CSSModules(component, styles, { handleNotFoundStyleName: 'log' })
}

View File

@@ -78,13 +78,13 @@ const languages = [
]
module.exports = {
getLocales () {
return languages.reduce(function (localeList, locale) {
getLocales() {
return languages.reduce(function(localeList, locale) {
localeList.push(locale.locale)
return localeList
}, [])
},
getLanguages () {
getLanguages() {
return languages
}
}

View File

@@ -1,43 +1,43 @@
class MutableMap {
constructor (iterable) {
constructor(iterable) {
this._map = new Map(iterable)
Object.defineProperty(this, 'size', {
get: () => this._map.size,
set: function (value) {
set: function(value) {
this['size'] = value
}
})
}
get (...args) {
get(...args) {
return this._map.get(...args)
}
set (...args) {
set(...args) {
return this._map.set(...args)
}
delete (...args) {
delete(...args) {
return this._map.delete(...args)
}
has (...args) {
has(...args) {
return this._map.has(...args)
}
clear (...args) {
clear(...args) {
return this._map.clear(...args)
}
forEach (...args) {
forEach(...args) {
return this._map.forEach(...args)
}
[Symbol.iterator] () {
[Symbol.iterator]() {
return this._map[Symbol.iterator]()
}
map (cb) {
map(cb) {
const result = []
for (const [key, value] of this._map) {
result.push(cb(value, key))
@@ -45,7 +45,7 @@ class MutableMap {
return result
}
toJS () {
toJS() {
const result = {}
for (let [key, value] of this._map) {
if (value instanceof MutableSet || value instanceof MutableMap) {
@@ -58,42 +58,42 @@ class MutableMap {
}
class MutableSet {
constructor (iterable) {
constructor(iterable) {
this._set = new Set(iterable)
Object.defineProperty(this, 'size', {
get: () => this._set.size,
set: function (value) {
set: function(value) {
this['size'] = value
}
})
}
add (...args) {
add(...args) {
return this._set.add(...args)
}
delete (...args) {
delete(...args) {
return this._set.delete(...args)
}
forEach (...args) {
forEach(...args) {
return this._set.forEach(...args)
}
[Symbol.iterator] () {
[Symbol.iterator]() {
return this._set[Symbol.iterator]()
}
map (cb) {
map(cb) {
const result = []
this._set.forEach(function (value, key) {
this._set.forEach(function(value, key) {
result.push(cb(value, key))
})
return result
}
toJS () {
toJS() {
return Array.from(this._set)
}
}

View File

@@ -5,13 +5,13 @@ const BOOSTNOTERC = '.boostnoterc'
const homePath = global.process.env.HOME || global.process.env.USERPROFILE
const _boostnotercPath = path.join(homePath, BOOSTNOTERC)
export function parse (boostnotercPath = _boostnotercPath) {
export function parse(boostnotercPath = _boostnotercPath) {
if (!sander.existsSync(boostnotercPath)) return {}
try {
return JSON.parse(sander.readFileSync(boostnotercPath).toString())
} catch (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 {}
}
}

View File

@@ -3,13 +3,14 @@ import fs from 'fs'
import consts from './consts'
class SnippetManager {
constructor () {
constructor() {
this.defaultSnippet = [
{
id: crypto.randomBytes(16).toString('hex'),
name: 'Dummy text',
prefix: ['lorem', 'ipsum'],
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
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 = []
@@ -18,7 +19,7 @@ class SnippetManager {
this.assignSnippets = this.assignSnippets.bind(this)
}
init () {
init() {
if (fs.existsSync(consts.SNIPPET_FILE)) {
try {
this.snippets = JSON.parse(
@@ -37,11 +38,11 @@ class SnippetManager {
this.snippets = this.defaultSnippet
}
assignSnippets (snippets) {
assignSnippets(snippets) {
this.snippets = snippets
}
expandSnippet (wordBeforeCursor, cursor, cm) {
expandSnippet(wordBeforeCursor, cursor, cm) {
const templateCursorString = ':{}'
for (let i = 0; i < this.snippets.length; i++) {
if (this.snippets[i].prefix.indexOf(wordBeforeCursor.text) === -1) {

View File

@@ -1,44 +1,44 @@
import { Point } from '@susisu/mte-kernel'
export default class TextEditorInterface {
constructor (editor) {
constructor(editor) {
this.editor = editor
this.doc = editor.getDoc()
this.transaction = false
}
getCursorPosition () {
getCursorPosition() {
const { line, ch } = this.doc.getCursor()
return new Point(line, ch)
}
setCursorPosition (pos) {
setCursorPosition(pos) {
this.doc.setCursor({
line: pos.row,
ch: pos.column
})
}
setSelectionRange (range) {
setSelectionRange(range) {
this.doc.setSelection(
{ line: range.start.row, ch: range.start.column },
{ line: range.end.row, ch: range.end.column }
)
}
getLastRow () {
getLastRow() {
return this.doc.lineCount() - 1
}
acceptsTableEdit () {
acceptsTableEdit() {
return true
}
getLine (row) {
getLine(row) {
return this.doc.getLine(row)
}
insertLine (row, line) {
insertLine(row, line) {
const lastRow = this.getLastRow()
if (row > lastRow) {
const lastLine = this.getLine(lastRow)
@@ -56,7 +56,7 @@ export default class TextEditorInterface {
}
}
deleteLine (row) {
deleteLine(row) {
const lastRow = this.getLastRow()
if (row >= lastRow) {
if (lastRow > 0) {
@@ -76,15 +76,11 @@ export default class TextEditorInterface {
)
}
} else {
this.doc.replaceRange(
'',
{ line: row, ch: 0 },
{ line: row + 1, ch: 0 }
)
this.doc.replaceRange('', { line: row, ch: 0 }, { line: row + 1, ch: 0 })
}
}
replaceLines (startRow, endRow, lines) {
replaceLines(startRow, endRow, lines) {
const lastRow = this.getLastRow()
if (endRow > lastRow) {
const lastLine = this.getLine(lastRow)
@@ -102,7 +98,7 @@ export default class TextEditorInterface {
}
}
transact (func) {
transact(func) {
this.transaction = true
func()
this.transaction = false

View File

@@ -3,7 +3,7 @@ import i18n from 'browser/lib/i18n'
const { remote } = electron
const { dialog } = remote
export function confirmDeleteNote (confirmDeletion, permanent) {
export function confirmDeleteNote(confirmDeletion, permanent) {
if (confirmDeletion || permanent) {
const alertConfig = {
type: 'warning',
@@ -13,7 +13,8 @@ export function confirmDeleteNote (confirmDeletion, permanent) {
}
const dialogButtonIndex = dialog.showMessageBox(
remote.getCurrentWindow(), alertConfig
remote.getCurrentWindow(),
alertConfig
)
return dialogButtonIndex === 0

View File

@@ -9,41 +9,53 @@ const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme'
const isProduction = process.env.NODE_ENV === 'production'
const paths = [
isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH),
isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH)
isProduction
? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH)
: path.resolve(CODEMIRROR_THEME_PATH),
isProduction
? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH)
: path.resolve(CODEMIRROR_EXTRA_THEME_PATH)
]
const themes = paths
.map(directory => fs.readdirSync(directory).map(file => {
const name = file.substring(0, file.lastIndexOf('.'))
.map(directory =>
fs.readdirSync(directory).map(file => {
const name = file.substring(0, file.lastIndexOf('.'))
return {
name,
path: path.join(directory, file),
className: `cm-s-${name}`
}
}))
return {
name,
path: path.join(directory, file),
className: `cm-s-${name}`
}
})
)
.reduce((accumulator, value) => accumulator.concat(value), [])
.sort((a, b) => a.name.localeCompare(b.name))
themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, {
name: 'solarized dark',
path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-dark`
}, {
name: 'solarized light',
path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-light`
})
themes.splice(
themes.findIndex(({ name }) => name === 'solarized'),
1,
{
name: 'solarized dark',
path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-dark`
},
{
name: 'solarized light',
path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-light`
}
)
themes.splice(0, 0, {
name: 'default',
path: path.join(paths[0], 'elegant.css'),
className: `cm-s-default`
})
const snippetFile = process.env.NODE_ENV !== 'test'
? path.join(app.getPath('userData'), 'snippets.json')
: '' // return nothing as we specified different path to snippets.json in test
const snippetFile =
process.env.NODE_ENV !== 'test'
? path.join(app.getPath('userData'), 'snippets.json')
: '' // return nothing as we specified different path to snippets.json in test
const consts = {
FOLDER_COLORS: [

View File

@@ -1,9 +1,9 @@
const { remote } = require('electron')
const { Menu, MenuItem } = remote
function popup (templates) {
function popup(templates) {
const menu = new Menu()
templates.forEach((item) => {
templates.forEach(item => {
menu.append(new MenuItem(item))
})
menu.popup(remote.getCurrentWindow())

View File

@@ -1,10 +1,10 @@
import i18n from 'browser/lib/i18n'
import fs from 'fs'
const {remote} = require('electron')
const {Menu} = remote.require('electron')
const {clipboard} = remote.require('electron')
const {shell} = remote.require('electron')
const { remote } = require('electron')
const { Menu } = remote.require('electron')
const { clipboard } = remote.require('electron')
const { shell } = remote.require('electron')
const spellcheck = require('./spellcheck')
const uri2path = require('file-uri-to-path')
@@ -16,11 +16,16 @@ const uri2path = require('file-uri-to-path')
* @param {MouseEvent} event that has triggered the creation of the context menu
* @returns {Electron.Menu} The created electron context menu
*/
const buildEditorContextMenu = function (editor, event) {
if (editor == null || event == null || event.pageX == null || event.pageY == null) {
const buildEditorContextMenu = function(editor, event) {
if (
editor == null ||
event == null ||
event.pageX == null ||
event.pageY == null
) {
return null
}
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY})
const cursor = editor.coordsChar({ left: event.pageX, top: event.pageY })
const wordRange = editor.findWordAt(cursor)
const word = editor.getRange(wordRange.anchor, wordRange.head)
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
@@ -40,30 +45,44 @@ const buildEditorContextMenu = function (editor, event) {
isMisspelled: isMisspelled,
spellingSuggestions: suggestion
}
const template = [{
role: 'cut'
}, {
role: 'copy'
}, {
role: 'paste'
}, {
role: 'selectall'
}]
const template = [
{
role: 'cut'
},
{
role: 'copy'
},
{
role: 'paste'
},
{
role: 'selectall'
}
]
if (selection.isMisspelled) {
const suggestions = selection.spellingSuggestions
template.unshift.apply(template, suggestions.map(function (suggestion) {
return {
label: suggestion,
click: function (suggestion) {
if (editor != null) {
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head)
template.unshift.apply(
template,
suggestions
.map(function(suggestion) {
return {
label: suggestion,
click: function(suggestion) {
if (editor != null) {
editor.replaceRange(
suggestion.label,
wordRange.anchor,
wordRange.head
)
}
}
}
}
}
}).concat({
type: 'separator'
}))
})
.concat({
type: 'separator'
})
)
}
return Menu.buildFromTemplate(template)
}
@@ -74,19 +93,30 @@ const buildEditorContextMenu = function (editor, event) {
* @param {MouseEvent} event that has triggered the creation of the context menu
* @returns {Electron.Menu} The created electron context menu
*/
const buildMarkdownPreviewContextMenu = function (markdownPreview, event) {
if (markdownPreview == null || event == null || event.pageX == null || event.pageY == null) {
const buildMarkdownPreviewContextMenu = function(markdownPreview, event) {
if (
markdownPreview == null ||
event == null ||
event.pageX == null ||
event.pageY == null
) {
return null
}
// Default context menu inclusions
const template = [{
role: 'copy'
}, {
role: 'selectall'
}]
const template = [
{
role: 'copy'
},
{
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
const href = event.target.href
const isLocalFile = href.startsWith('file:')
@@ -94,31 +124,29 @@ const buildMarkdownPreviewContextMenu = function (markdownPreview, event) {
const absPath = uri2path(href)
try {
if (fs.lstatSync(absPath).isFile()) {
template.push(
{
label: i18n.__('Show in explorer'),
click: (e) => shell.showItemInFolder(absPath)
}
)
template.push({
label: i18n.__('Show in explorer'),
click: e => shell.showItemInFolder(absPath)
})
}
} 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
template.push(
{
label: i18n.__('Copy Url'),
click: (e) => clipboard.writeText(href)
}
)
template.push({
label: i18n.__('Copy Url'),
click: e => clipboard.writeText(href)
})
}
return Menu.buildFromTemplate(template)
}
module.exports =
{
module.exports = {
buildEditorContextMenu: buildEditorContextMenu,
buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu
}

View File

@@ -1,4 +1,4 @@
export default function convertModeName (name) {
export default function convertModeName(name) {
switch (name) {
case 'ejs':
return 'Embedded Javascript'

View File

@@ -3,8 +3,19 @@ import 'codemirror-mode-elixir'
const stylusCodeInfo = CodeMirror.modeInfo.find(info => info.name === 'Stylus')
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 {
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']
})

View File

@@ -8,7 +8,7 @@ import moment from 'moment'
* @param {mixed}
* @return {string}
*/
export function formatDate (date) {
export function formatDate(date) {
const m = moment(date)
if (!m.isValid()) {
throw Error('Invalid argument.')

View File

@@ -1,4 +1,8 @@
export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') {
export function findNoteTitle(
value,
enableFrontMatterTitle,
frontMatterTitleField = 'title'
) {
const splitted = value.split('\n')
let title = null
let isInsideCodeBlock = false
@@ -6,8 +10,13 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
if (splitted[0] === '---') {
let line = 0
while (++line < splitted.length) {
if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) {
title = splitted[line].substring(frontMatterTitleField.length + 1).trim()
if (
enableFrontMatterTitle &&
splitted[line].startsWith(frontMatterTitleField + ':')
) {
title = splitted[line]
.substring(frontMatterTitleField.length + 1)
.trim()
break
}
@@ -22,11 +31,15 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
if (title === null) {
splitted.some((line, index) => {
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('```')) {
isInsideCodeBlock = !isInsideCodeBlock
}
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
if (
isInsideCodeBlock === false &&
(trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))
) {
title = trimmedLine
return true
}
@@ -35,7 +48,7 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
if (title === null) {
title = ''
splitted.some((line) => {
splitted.some(line => {
if (line.trim().length > 0) {
title = line.trim()
return true

View File

@@ -1,10 +1,11 @@
const _ = require('lodash')
export function findStorage (storageKey) {
export function findStorage(storageKey) {
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
const storage = _.find(cachedStorageList, {key: storageKey})
if (storage === undefined) 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 })
if (storage === undefined) throw new Error("Target storage doesn't exist.")
return storage
}

View File

@@ -1,9 +1,9 @@
export function getTodoStatus (content) {
export function getTodoStatus(content) {
const splitted = content.split('\n')
let numberOfTodo = 0
let numberOfCompletedTodo = 0
splitted.forEach((line) => {
splitted.forEach(line => {
const trimmedLine = line.trim().replace(/^(>\s*)*/, '')
if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) {
numberOfTodo++
@@ -19,7 +19,7 @@ export function getTodoStatus (content) {
}
}
export function getTodoPercentageOfCompleted (content) {
export function getTodoPercentageOfCompleted(content) {
const state = getTodoStatus(content)
return Math.floor(state.completed / state.total * 100)
return Math.floor((state.completed / state.total) * 100)
}

View File

@@ -7,9 +7,9 @@
* @return {string}
*/
export function decodeEntities (text) {
export function decodeEntities(text) {
var entities = [
['apos', '\''],
['apos', "'"],
['amp', '&'],
['lt', '<'],
['gt', '>'],
@@ -24,16 +24,16 @@ export function decodeEntities (text) {
return text
}
export function encodeEntities (text) {
export function encodeEntities(text) {
const entities = [
['\'', 'apos'],
["'", 'apos'],
['<', 'lt'],
['>', 'gt'],
['\\?', '#63'],
['\\$', '#36']
]
entities.forEach((entity) => {
entities.forEach(entity => {
text = text.replace(new RegExp(entity[0], 'g'), `&${entity[1]};`)
})
return text

View File

@@ -8,9 +8,10 @@ const i18n = new (require('i18n-2'))({
// setup some locales - other locales default to the first locale
locales: getLocales(),
extension: '.json',
directory: process.env.NODE_ENV === 'production'
? path.join(app.getAppPath(), './locales')
: path.resolve('./locales'),
directory:
process.env.NODE_ENV === 'production'
? path.join(app.getAppPath(), './locales')
: path.resolve('./locales'),
devMode: false
})

View File

@@ -1,7 +1,7 @@
const crypto = require('crypto')
const uuidv4 = require('uuid/v4')
module.exports = function (uuid) {
module.exports = function(uuid) {
if (typeof uuid === typeof true && uuid) {
return uuidv4()
}

View File

@@ -1,35 +1,44 @@
'use strict'
module.exports = function definitionListPlugin (md) {
module.exports = function definitionListPlugin(md) {
var isSpace = md.utils.isSpace
// Search `[:~][\n ]`, returns next pos after marker on success
// or -1 on fail.
function skipMarker (state, line) {
function skipMarker(state, line) {
let start = state.bMarks[line] + state.tShift[line]
const max = state.eMarks[line]
if (start >= max) { return -1 }
if (start >= max) {
return -1
}
// Check bullet
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)
// require space after ":"
if (start === pos) { return -1 }
if (start === pos) {
return -1
}
return start
}
function markTightParagraphs (state, idx) {
function markTightParagraphs(state, idx) {
const level = state.level + 2
let i
let l
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].hidden = true
i += 2
@@ -37,7 +46,7 @@ module.exports = function definitionListPlugin (md) {
}
}
function deflist (state, startLine, endLine, silent) {
function deflist(state, startLine, endLine, silent) {
var ch,
contentStart,
ddLine,
@@ -63,28 +72,38 @@ module.exports = function definitionListPlugin (md) {
if (silent) {
// 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
}
nextLine = startLine + 1
if (nextLine >= endLine) { return false }
if (nextLine >= endLine) {
return false
}
if (state.isEmpty(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)
if (contentStart < 0) { return false }
if (contentStart < 0) {
return false
}
// Start list
listTokIdx = state.tokens.length
tight = true
token = state.push('dl_open', 'dl', 1)
token.map = listLines = [ startLine, 0 ]
token.map = listLines = [startLine, 0]
//
// Iterate list items
@@ -100,34 +119,38 @@ module.exports = function definitionListPlugin (md) {
// needed to break out of the second one
//
/* eslint no-labels:0,block-scoped-var:0 */
OUTER:
for (;;) {
OUTER: for (;;) {
prevEmptyEnd = false
token = state.push('dt_open', 'dt', 1)
token.map = [ dtLine, dtLine ]
token.map = [dtLine, dtLine]
token = state.push('inline', '', 0)
token.map = [ dtLine, dtLine ]
token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim()
token.map = [dtLine, dtLine]
token.content = state
.getLines(dtLine, dtLine + 1, state.blkIndent, false)
.trim()
token.children = []
token = state.push('dt_close', 'dt', -1)
for (;;) {
token = state.push('dd_open', 'dd', 1)
token.map = itemLines = [ ddLine, 0 ]
token.map = itemLines = [ddLine, 0]
pos = contentStart
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) {
ch = state.src.charCodeAt(pos)
if (isSpace(ch)) {
if (ch === 0x09) {
offset += 4 - offset % 4
offset += 4 - (offset % 4)
} else {
offset++
}
@@ -153,8 +176,11 @@ module.exports = function definitionListPlugin (md) {
state.parentType = 'deflist'
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
state.lineMax = newEndLine
@@ -169,7 +195,7 @@ module.exports = function definitionListPlugin (md) {
}
// Item become loose if finish with empty line,
// 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.sCount[ddLine] = oldSCount
@@ -182,11 +208,17 @@ module.exports = function definitionListPlugin (md) {
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)
if (contentStart < 0) { break }
if (contentStart < 0) {
break
}
ddLine = nextLine
@@ -194,20 +226,36 @@ module.exports = function definitionListPlugin (md) {
// insert DD tag and repeat checking
}
if (nextLine >= endLine) { break }
if (nextLine >= endLine) {
break
}
dtLine = nextLine
if (state.isEmpty(dtLine)) { break }
if (state.sCount[dtLine] < state.blkIndent) { break }
if (state.isEmpty(dtLine)) {
break
}
if (state.sCount[dtLine] < state.blkIndent) {
break
}
ddLine = dtLine + 1
if (ddLine >= endLine) { break }
if (state.isEmpty(ddLine)) { ddLine++ }
if (ddLine >= endLine) { 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)
if (contentStart < 0) { break }
if (contentStart < 0) {
break
}
// go to the next loop iteration:
// insert DT and DD tags and repeat checking
@@ -228,5 +276,7 @@ module.exports = function definitionListPlugin (md) {
return true
}
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] })
md.block.ruler.before('paragraph', 'deflist', deflist, {
alt: ['paragraph', 'reference']
})
}

View File

@@ -1,9 +1,9 @@
'use strict'
module.exports = function (md, renderers, defaultRenderer) {
module.exports = function(md, renderers, defaultRenderer) {
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
function fence (state, startLine, endLine, silent) {
function fence(state, startLine, endLine, silent) {
let pos = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine]
@@ -12,7 +12,7 @@ module.exports = function (md, renderers, defaultRenderer) {
}
const marker = state.src.charCodeAt(pos)
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) {
if (marker !== 0x7e /* ~ */ && marker !== 0x60 /* ` */) {
return false
}
@@ -46,7 +46,10 @@ module.exports = function (md, renderers, defaultRenderer) {
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
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
}
@@ -127,10 +130,12 @@ module.exports = function (md, renderers, defaultRenderer) {
})
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) {
md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index])
md.renderer.rules['_fence'] = (tokens, index) =>
defaultRenderer(tokens[index])
}
}

View File

@@ -1,14 +1,19 @@
'use strict'
module.exports = function frontMatterPlugin (md) {
function frontmatter (state, startLine, endLine, silent) {
if (startLine !== 0 || state.src.substr(startLine, state.eMarks[0]) !== '---') {
module.exports = function frontMatterPlugin(md) {
function frontmatter(state, startLine, endLine, silent) {
if (
startLine !== 0 ||
state.src.substr(startLine, state.eMarks[0]) !== '---'
) {
return false
}
let line = 0
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
return true
@@ -19,6 +24,6 @@ module.exports = function frontMatterPlugin (md) {
}
md.block.ruler.before('table', 'frontmatter', frontmatter, {
alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
alt: ['paragraph', 'reference', 'blockquote', 'list']
})
}

View File

@@ -4,7 +4,7 @@ import sanitizeHtml from 'sanitize-html'
import { escapeHtmlCharacters } from './utils'
import url from 'url'
module.exports = function sanitizePlugin (md, options) {
module.exports = function sanitizePlugin(md, options) {
options = options || {}
md.core.ruler.after('linkify', 'sanitize_inline', state => {
@@ -38,15 +38,20 @@ 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 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)
if (!match) {
return ''
}
const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options
const {
allowedTags,
allowedAttributes,
selfClosing,
allowedSchemesAppliedToAttributes
} = options
if (match[1] !== undefined) {
// opening tag
@@ -65,9 +70,17 @@ function sanitizeInline (html, options) {
name = match[1].toLowerCase()
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 (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) {
if (
naughtyHRef(value, options) ||
(tag === 'iframe' &&
name === 'src' &&
naughtyIFrame(value, options))
) {
continue
}
}
@@ -94,7 +107,7 @@ function sanitizeInline (html, options) {
}
}
function naughtyHRef (href, options) {
function naughtyHRef(href, options) {
// href = href.replace(/[\x00-\x20]+/g, '')
if (!href) {
// No href
@@ -117,7 +130,7 @@ function naughtyHRef (href, options) {
return options.allowedSchemes.indexOf(scheme) === -1
}
function naughtyIFrame (src, options) {
function naughtyIFrame(src, options) {
try {
const parsed = url.parse(src, false, true)

View File

@@ -12,7 +12,7 @@ const hasProp = Object.prototype.hasOwnProperty
/**
* From @enyaxu/markdown-it-anchor
*/
function uniqueSlug (slug, slugs, opts) {
function uniqueSlug(slug, slugs, opts) {
let uniq = slug
let i = opts.uniqueSlugStartIndex
while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
@@ -20,7 +20,7 @@ function uniqueSlug (slug, slugs, opts) {
return uniq
}
function linkify (token) {
function linkify(token) {
token.content = mdlink(token.content, `#${decodeURI(token.slug)}`)
return token
}
@@ -36,8 +36,8 @@ const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
* Otherwise,TOC is updated in place.
* @param editor CodeMirror editor to be updated with TOC
*/
export function generateInEditor (editor) {
function updateExistingToc () {
export function generateInEditor(editor) {
function updateExistingToc() {
const toc = generate(editor.getValue())
const search = editor.getSearchCursor(tocRegex)
while (search.findNext()) {
@@ -45,8 +45,10 @@ export function generateInEditor (editor) {
}
}
function addTocAtCursorPosition () {
const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity}))
function addTocAtCursorPosition() {
const toc = generate(
editor.getRange(editor.getCursor(), { line: Infinity })
)
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
}
@@ -57,7 +59,7 @@ export function generateInEditor (editor) {
}
}
export function tocExistsInEditor (editor) {
export function tocExistsInEditor(editor) {
return tocRegex.test(editor.getValue())
}
@@ -66,7 +68,7 @@ export function tocExistsInEditor (editor) {
* @param markdownText MD document
* @returns generatedTOC String containing generated TOC
*/
export function generate (markdownText) {
export function generate(markdownText) {
const slugs = {}
const opts = {
uniqueSlugStartIndex: 1
@@ -86,9 +88,12 @@ export function generate (markdownText) {
return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
}
function wrapTocWithEol (toc, editor) {
function wrapTocWithEol(toc, editor) {
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
}

View File

@@ -10,18 +10,20 @@ import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
import { lastFindInArray } from './utils'
function createGutter (str, firstLineNumber) {
function createGutter(str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
const lines = []
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
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 {
constructor (options = {}) {
constructor(options = {}) {
const config = ConfigManager.get()
const defaultOptions = {
typographer: config.preview.smartQuotes,
@@ -37,29 +39,129 @@ class Markdown {
this.md.linkify.set({ fuzzyLink: false })
if (updatedOptions.sanitize !== 'NONE') {
const allowedTags = ['iframe', 'input', '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 allowedTags = [
'iframe',
'input',
'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 = [
'abbr', 'accept', 'accept-charset',
'accesskey', 'action', 'align', 'alt', 'axis',
'border', 'cellpadding', 'cellspacing', 'char',
'charoff', 'charset', 'checked',
'clear', 'cols', 'colspan', 'color',
'compact', 'coords', 'datetime', 'dir',
'disabled', 'enctype', 'for', 'frame',
'headers', 'height', 'hreflang',
'hspace', 'ismap', 'label', 'lang',
'maxlength', 'media', 'method',
'multiple', 'name', 'nohref', 'noshade',
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
'rows', 'rowspan', 'rules', 'scope',
'selected', 'shape', 'size', 'span',
'start', 'summary', 'tabindex', 'target',
'title', 'type', 'usemap', 'valign', 'value',
'vspace', 'width', 'itemprop'
'abbr',
'accept',
'accept-charset',
'accesskey',
'action',
'align',
'alt',
'axis',
'border',
'cellpadding',
'cellspacing',
'char',
'charoff',
'charset',
'checked',
'clear',
'cols',
'colspan',
'color',
'compact',
'coords',
'datetime',
'dir',
'disabled',
'enctype',
'for',
'frame',
'headers',
'height',
'hreflang',
'hspace',
'ismap',
'label',
'lang',
'maxlength',
'media',
'method',
'multiple',
'name',
'nohref',
'noshade',
'nowrap',
'open',
'prompt',
'readonly',
'rel',
'rev',
'rows',
'rowspan',
'rules',
'scope',
'selected',
'shape',
'size',
'span',
'start',
'summary',
'tabindex',
'target',
'title',
'type',
'usemap',
'valign',
'value',
'vspace',
'width',
'itemprop'
]
if (updatedOptions.sanitize === 'ALLOW_STYLES') {
@@ -72,20 +174,20 @@ class Markdown {
allowedTags,
allowedAttributes: {
'*': allowedAttributes,
'a': ['href'],
'div': ['itemscope', 'itemtype'],
'blockquote': ['cite'],
'del': ['cite'],
'ins': ['cite'],
'q': ['cite'],
'img': ['src', 'width', 'height'],
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
'input': ['type', 'id', 'checked']
a: ['href'],
div: ['itemscope', 'itemtype'],
blockquote: ['cite'],
del: ['cite'],
ins: ['cite'],
q: ['cite'],
img: ['src', 'width', 'height'],
iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
input: ['type', 'id', 'checked']
},
allowedIframeHostnames: ['www.youtube.com'],
selfClosing: [ 'img', 'br', 'hr', 'input' ],
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
selfClosing: ['img', 'br', 'hr', 'input'],
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
allowProtocolRelative: true
})
}
@@ -98,7 +200,7 @@ class Markdown {
inlineClose: config.preview.latexInlineClose,
blockOpen: config.preview.latexBlockOpen,
blockClose: config.preview.latexBlockClose,
inlineRenderer: function (str) {
inlineRenderer: function(str) {
let output = ''
try {
output = katex.renderToString(str.trim())
@@ -107,7 +209,7 @@ class Markdown {
}
return output
},
blockRenderer: function (str) {
blockRenderer: function(str) {
let output = ''
try {
output = katex.renderToString(str.trim(), { displayMode: true })
@@ -124,7 +226,19 @@ class Markdown {
slugify: require('./slugify')
})
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-sub'))
this.md.use(require('markdown-it-sup'))
@@ -144,63 +258,86 @@ class Markdown {
this.md.use(require('./markdown-it-deflist'))
this.md.use(require('./markdown-it-frontmatter'))
this.md.use(require('./markdown-it-fence'), {
chart: token => {
if (token.parameters.hasOwnProperty('yaml')) {
token.parameters.format = 'yaml'
}
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="chart" data-height="${token.parameters.height}" data-format="${token.parameters.format || 'json'}">${token.content}</div>
</pre>`
},
flowchart: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
</pre>`
},
gallery: token => {
const content = token.content.split('\n').slice(0, -1).map(line => {
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
if (match) {
return mdurl.encode(match[1])
} else {
return mdurl.encode(line)
this.md.use(
require('./markdown-it-fence'),
{
chart: token => {
if (token.parameters.hasOwnProperty('yaml')) {
token.parameters.format = 'yaml'
}
}).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>
<div class="gallery" data-autoplay="${token.parameters.autoplay}" data-height="${token.parameters.height}">${content}</div>
<div class="chart" data-height="${
token.parameters.height
}" data-format="${token.parameters.format || 'json'}">${
token.content
}</div>
</pre>`
},
flowchart: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="flowchart" data-height="${token.parameters.height}">${
token.content
}</div>
</pre>`
},
gallery: token => {
const content = token.content
.split('\n')
.slice(0, -1)
.map(line => {
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
if (match) {
return mdurl.encode(match[1])
} else {
return mdurl.encode(line)
}
})
.join('\n')
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="gallery" data-autoplay="${
token.parameters.autoplay
}" data-height="${token.parameters.height}">${content}</div>
</pre>`
},
mermaid: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="mermaid" data-height="${token.parameters.height}">${
token.content
}</div>
</pre>`
},
sequence: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="sequence" data-height="${token.parameters.height}">${
token.content
}</div>
</pre>`
}
},
mermaid: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="mermaid" data-height="${token.parameters.height}">${token.content}</div>
</pre>`
},
sequence: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="sequence" data-height="${token.parameters.height}">${token.content}</div>
</pre>`
}
}, token => {
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
token => {
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
${createGutter(token.content, token.firstLineNumber)}
<code class="${token.langType}">${token.content}</code>
</pre>`
})
}
)
const deflate = require('markdown-it-plantuml/lib/deflate')
const plantuml = require('markdown-it-plantuml')
const plantUmlStripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
const plantUmlServerAddress = plantUmlStripTrailingSlash(config.preview.plantUMLServerAddress)
const parsePlantUml = function (umlCode, openMarker, closeMarker, type) {
const plantUmlStripTrailingSlash = url =>
url.endsWith('/') ? url.slice(0, -1) : url
const plantUmlServerAddress = plantUmlStripTrailingSlash(
config.preview.plantUMLServerAddress
)
const parsePlantUml = function(umlCode, openMarker, closeMarker, type) {
const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64(
deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9)
@@ -209,39 +346,47 @@ class Markdown {
}
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.
this.md.use(plantuml, {
openMarker: '@startditaa',
closeMarker: '@endditaa',
generateSource: (umlCode) => parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
generateSource: umlCode =>
parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
})
// Mindmap support
this.md.use(plantuml, {
openMarker: '@startmindmap',
closeMarker: '@endmindmap',
generateSource: (umlCode) => parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg')
generateSource: umlCode =>
parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg')
})
// WBS support
this.md.use(plantuml, {
openMarker: '@startwbs',
closeMarker: '@endwbs',
generateSource: (umlCode) => parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg')
generateSource: umlCode =>
parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg')
})
// Gantt support
this.md.use(plantuml, {
openMarker: '@startgantt',
closeMarker: '@endgantt',
generateSource: (umlCode) => parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg')
generateSource: umlCode =>
parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg')
})
// 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 nextLine = startLine + 1
const terminatorRules = state.md.block.ruler.getRules('paragraph')
@@ -251,10 +396,14 @@ class Markdown {
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
// this would be a code block normally, but after paragraph
// it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
if (state.sCount[nextLine] - state.blkIndent > 3) {
continue
}
// quirk for blockquotes, this line should already be checked by that rule
if (state.sCount[nextLine] < 0) { continue }
if (state.sCount[nextLine] < 0) {
continue
}
// Some tags can terminate paragraph without empty line.
terminate = false
@@ -264,10 +413,14 @@ class Markdown {
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
@@ -277,18 +430,31 @@ class Markdown {
if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i)
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.attrs) {
liToken.attrs = []
}
if (config.preview.lineThroughCheckbox) {
liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`])
liToken.attrs.push([
'class',
`taskListItem${match[1] !== ' ' ? ' checked' : ''}`
])
} else {
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
const originalRender = this.md.renderer.render
this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => {
tokens.forEach(token => {
switch (token.type) {
case 'blockquote_open':
case 'dd_open':
@@ -330,7 +496,7 @@ class Markdown {
window.md = this.md
}
render (content) {
render(content) {
if (!_.isString(content)) content = ''
return this.md.render(content)
}

View File

@@ -6,7 +6,7 @@
* @param {string} input
* @return {string}
*/
export function strip (input) {
export function strip(input) {
let output = input
try {
output = output

View File

@@ -4,12 +4,22 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import queryString from 'query-string'
import { push } from 'connected-react-router'
export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
export function createMarkdownNote(
storage,
folder,
dispatch,
location,
params,
config
) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let tags = []
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
if (
config.ui.tagNewNoteWithFilteringTags &&
location.pathname.match(/\/tags/)
) {
tags = params.tagname.split(' ')
}
@@ -29,25 +39,40 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
note: note
})
dispatch(push({
pathname: location.pathname,
search: queryString.stringify({ key: noteHash })
}))
dispatch(
push({
pathname: location.pathname,
search: queryString.stringify({ key: noteHash })
})
)
ee.emit('list:jump', noteHash)
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_ALLNOTE')
let tags = []
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
if (
config.ui.tagNewNoteWithFilteringTags &&
location.pathname.match(/\/tags/)
) {
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
.createNote(storage, {
@@ -71,10 +96,12 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
type: 'UPDATE_NOTE',
note: note
})
dispatch(push({
pathname: location.pathname,
search: queryString.stringify({ key: noteHash })
}))
dispatch(
push({
pathname: location.pathname,
search: queryString.stringify({ key: noteHash })
})
)
ee.emit('list:jump', noteHash)
ee.emit('detail:focus')
})

View File

@@ -1,7 +1,7 @@
import consts from 'browser/lib/consts'
import isString from 'lodash/isString'
export default function normalizeEditorFontFamily (fontFamily) {
export default function normalizeEditorFontFamily(fontFamily) {
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
return isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily).join(', ')

View File

@@ -1,31 +1,38 @@
import _ from 'lodash'
export default function searchFromNotes (notes, search) {
export default function searchFromNotes(notes, search) {
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
searchBlocks.forEach((block) => {
searchBlocks.forEach(block => {
foundNotes = findByWordOrTag(foundNotes, block)
})
return foundNotes
}
function findByWordOrTag (notes, block) {
function findByWordOrTag(notes, block) {
let tag = block
if (tag.match(/^#.+/)) {
tag = tag.match(/#(.+)/)[1]
}
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
return notes.filter((note) => {
if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) {
return notes.filter(note => {
if (_.isArray(note.tags) && note.tags.some(_tag => _tag.match(tagRegExp))) {
return true
}
if (note.type === 'SNIPPET_NOTE') {
return note.description.match(wordRegExp) || note.snippets.some((snippet) => {
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
})
return (
note.description.match(wordRegExp) ||
note.snippets.some(snippet => {
return (
snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
)
})
)
} else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(wordRegExp)
}

View File

@@ -1,10 +1,14 @@
module.exports = function slugify (title) {
module.exports = function slugify(title) {
const slug = encodeURI(
title.trim()
title
.trim()
.replace(/^\s+/, '')
.replace(/\s+$/, '')
.replace(/\s+/g, '-')
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g, '')
.replace(
/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g,
''
)
)
return slug

View File

@@ -12,19 +12,19 @@ const MILLISECONDS_TILL_LIVECHECK = 500
let dictionary = null
let self
function getAvailableDictionaries () {
function getAvailableDictionaries() {
return [
{label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED},
{label: i18n.__('English'), value: 'en_GB'},
{label: i18n.__('German'), value: 'de_DE'},
{label: i18n.__('French'), value: 'fr_FR'}
{ label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED },
{ label: i18n.__('English'), value: 'en_GB' },
{ label: i18n.__('German'), value: 'de_DE' },
{ label: i18n.__('French'), value: 'fr_FR' }
]
}
/**
* Only to be used in the tests :)
*/
function setDictionaryForTestsOnly (newDictionary) {
function setDictionaryForTestsOnly(newDictionary) {
dictionary = newDictionary
}
@@ -34,7 +34,7 @@ function setDictionaryForTestsOnly (newDictionary) {
* @param {Codemirror} editor CodeMirror-Editor
* @param {String} lang on of the values from getAvailableDictionaries()-Method
*/
function setLanguage (editor, lang) {
function setLanguage(editor, lang) {
self = this
dictionary = null
@@ -50,8 +50,7 @@ function setLanguage (editor, lang) {
dictionary = new Typo(lang, false, false, {
dictionaryPath: DICTIONARY_PATH,
asyncLoad: true,
loadedCallback: () =>
checkWholeDocument(editor)
loadedCallback: () => checkWholeDocument(editor)
})
}
}
@@ -60,12 +59,12 @@ function setLanguage (editor, lang) {
* Checks the whole content of the editor for typos
* @param {Codemirror} editor CodeMirror-Editor
*/
function checkWholeDocument (editor) {
function checkWholeDocument(editor) {
const lastLine = editor.lineCount() - 1
const textOfLastLine = editor.getLine(lastLine) || ''
const lastChar = textOfLastLine.length
const from = {line: 0, ch: 0}
const to = {line: lastLine, ch: lastChar}
const from = { line: 0, ch: 0 }
const to = { line: lastLine, ch: lastChar }
checkMultiLineRange(editor, from, to)
}
@@ -75,15 +74,18 @@ function checkWholeDocument (editor) {
* @param {line, ch} from starting position of the spellcheck
* @param {line, ch} to end position of the spellcheck
*/
function checkMultiLineRange (editor, from, to) {
function sortRange (pos1, pos2) {
if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) {
return {from: pos2, to: pos1}
function checkMultiLineRange(editor, from, to) {
function sortRange(pos1, pos2) {
if (
pos1.line > pos2.line ||
(pos1.line === pos2.line && pos1.ch > pos2.ch)
) {
return { from: pos2, to: pos1 }
}
return {from: pos1, to: pos2}
return { from: pos1, to: pos2 }
}
const {from: smallerPos, to: higherPos} = sortRange(from, to)
const { from: smallerPos, to: higherPos } = sortRange(from, to)
for (let l = smallerPos.line; l <= higherPos.line; l++) {
const line = editor.getLine(l) || ''
let w = 0
@@ -95,9 +97,9 @@ function checkMultiLineRange (editor, from, to) {
wEnd = higherPos.ch
}
while (w <= wEnd) {
const wordRange = editor.findWordAt({line: l, ch: w})
const wordRange = editor.findWordAt({ line: l, ch: w })
self.checkWord(editor, wordRange)
w += (wordRange.head.ch - wordRange.anchor.ch) + 1
w += wordRange.head.ch - wordRange.anchor.ch + 1
}
}
}
@@ -110,13 +112,15 @@ function checkMultiLineRange (editor, from, to) {
* @param wordRange Object specifying the range that should be checked.
* Having the following structure: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
*/
function checkWord (editor, wordRange) {
function checkWord(editor, wordRange) {
const word = editor.getRange(wordRange.anchor, wordRange.head)
if (word == null || word.length <= 3) {
return
}
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]
})
}
}
@@ -126,32 +130,40 @@ function checkWord (editor, wordRange) {
* @param fromChangeObject codeMirror changeObject describing the start of the editing
* @param toChangeObject codeMirror changeObject describing the end of the editing
*/
function checkChangeRange (editor, fromChangeObject, toChangeObject) {
function checkChangeRange(editor, fromChangeObject, toChangeObject) {
/**
* Calculate the smallest respectively largest position as a start, resp. end, position and return it
* @param start CodeMirror change object
* @param end CodeMirror change object
* @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
*/
function getStartAndEnd (start, end) {
function getStartAndEnd(start, end) {
const possiblePositions = [start.from, start.to, end.from, end.to]
let smallest = start.from
let biggest = end.to
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
}
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
}
}
return {start: smallest, end: biggest}
return { start: smallest, end: biggest }
}
if (dictionary === null || editor == null) { return }
if (dictionary === null || editor == null) {
return
}
try {
const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject)
const { start, end } = getStartAndEnd(fromChangeObject, toChangeObject)
// Expand the range to include words after/before whitespaces
start.ch = Math.max(start.ch - 1, 0)
@@ -165,29 +177,40 @@ function checkChangeRange (editor, fromChangeObject, toChangeObject) {
self.checkMultiLineRange(editor, start, end)
} 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
)
}
}
function saveLiveSpellCheckFrom (changeObject) {
function saveLiveSpellCheckFrom(changeObject) {
liveSpellCheckFrom = changeObject
}
let liveSpellCheckFrom
const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, {
'leading': true,
'trailing': false
})
const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, {
'leading': false,
'trailing': true
})
const debouncedSpellCheckLeading = _.debounce(
saveLiveSpellCheckFrom,
MILLISECONDS_TILL_LIVECHECK,
{
leading: true,
trailing: false
}
)
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
* @param {Codemirror} editor CodeMirror-Editor
* @param changeObject codeMirror changeObject
*/
function handleChange (editor, changeObject) {
function handleChange(editor, changeObject) {
if (dictionary === null) {
return
}
@@ -201,7 +224,7 @@ function handleChange (editor, changeObject) {
* @param word word to be checked
* @returns {String[]} Array of suggestions
*/
function getSpellingSuggestion (word) {
function getSpellingSuggestion(word) {
if (dictionary == null || word == null) {
return []
}
@@ -211,7 +234,7 @@ function getSpellingSuggestion (word) {
/**
* Returns the name of the CSS class used for errors
*/
function getCSSClassName () {
function getCSSClassName() {
return styles[CSS_ERROR_CLASS]
}

View File

@@ -1,7 +1,7 @@
const TurndownService = require('turndown')
const { gfm } = require('turndown-plugin-gfm')
export const createTurndownService = function () {
export const createTurndownService = function() {
const turndown = new TurndownService()
turndown.use(gfm)
turndown.remove('script')

View File

@@ -1,4 +1,4 @@
export function lastFindInArray (array, callback) {
export function lastFindInArray(array, callback) {
for (let i = array.length - 1; i >= 0; --i) {
if (callback(array[i], i, array)) {
return array[i]
@@ -6,7 +6,7 @@ export function lastFindInArray (array, callback) {
}
}
export function escapeHtmlCharacters (
export function escapeHtmlCharacters(
html,
opt = { detectCodeBlock: false, skipSingleQuote: false }
) {
@@ -115,7 +115,7 @@ export function escapeHtmlCharacters (
return html
}
export function isObjectEqual (a, b) {
export function isObjectEqual(a, b) {
const aProps = Object.getOwnPropertyNames(a)
const bProps = Object.getOwnPropertyNames(b)
@@ -132,11 +132,13 @@ export function isObjectEqual (a, b) {
return true
}
export function isMarkdownTitleURL (str) {
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
export function isMarkdownTitleURL(str) {
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(
str
)
}
export function humanFileSize (bytes) {
export function humanFileSize(bytes) {
const threshold = 1000
if (Math.abs(bytes) < threshold) {
return bytes + ' B'

View File

@@ -6,7 +6,7 @@ import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class FolderSelect extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -16,24 +16,27 @@ class FolderSelect extends React.Component {
}
}
componentDidMount () {
componentDidMount() {
this.value = this.props.value
}
componentDidUpdate () {
componentDidUpdate() {
this.value = this.props.value
}
handleClick (e) {
this.setState({
status: 'SEARCH',
optionIndex: -1
}, () => {
this.refs.search.focus()
})
handleClick(e) {
this.setState(
{
status: 'SEARCH',
optionIndex: -1
},
() => {
this.refs.search.focus()
}
)
}
handleFocus (e) {
handleFocus(e) {
if (this.state.status === 'IDLE') {
this.setState({
status: 'FOCUS'
@@ -41,7 +44,7 @@ class FolderSelect extends React.Component {
}
}
handleBlur (e) {
handleBlur(e) {
if (this.state.status === 'FOCUS') {
this.setState({
status: 'IDLE'
@@ -49,40 +52,49 @@ class FolderSelect extends React.Component {
}
}
handleKeyDown (e) {
handleKeyDown(e) {
switch (e.keyCode) {
case 13:
if (this.state.status === 'FOCUS') {
this.setState({
status: 'SEARCH',
optionIndex: -1
}, () => {
this.refs.search.focus()
})
this.setState(
{
status: 'SEARCH',
optionIndex: -1
},
() => {
this.refs.search.focus()
}
)
}
break
case 40:
case 38:
if (this.state.status === 'FOCUS') {
this.setState({
status: 'SEARCH',
optionIndex: 0
}, () => {
this.refs.search.focus()
})
this.setState(
{
status: 'SEARCH',
optionIndex: 0
},
() => {
this.refs.search.focus()
}
)
}
break
case 9:
if (e.shiftKey) {
e.preventDefault()
const tabbable = document.querySelectorAll('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]
const tabbable = document.querySelectorAll(
'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()
}
}
}
handleSearchInputBlur (e) {
handleSearchInputBlur(e) {
if (e.relatedTarget !== this.refs.root) {
this.setState({
status: 'IDLE'
@@ -90,14 +102,17 @@ class FolderSelect extends React.Component {
}
}
handleSearchInputChange (e) {
handleSearchInputChange(e) {
const { folders } = this.props
const search = this.refs.search.value
const optionIndex = search.length > 0
? _.findIndex(folders, (folder) => {
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
})
: -1
const optionIndex =
search.length > 0
? _.findIndex(folders, folder => {
return folder.name.match(
new RegExp('^' + _.escapeRegExp(search), 'i')
)
})
: -1
this.setState({
search: this.refs.search.value,
@@ -105,7 +120,7 @@ class FolderSelect extends React.Component {
})
}
handleSearchInputKeyDown (e) {
handleSearchInputKeyDown(e) {
switch (e.keyCode) {
case 40:
e.stopPropagation()
@@ -121,15 +136,18 @@ class FolderSelect extends React.Component {
break
case 27:
e.stopPropagation()
this.setState({
status: 'FOCUS'
}, () => {
this.refs.root.focus()
})
this.setState(
{
status: 'FOCUS'
},
() => {
this.refs.root.focus()
}
)
}
}
nextOption () {
nextOption() {
let { optionIndex } = this.state
const { folders } = this.props
@@ -141,7 +159,7 @@ class FolderSelect extends React.Component {
})
}
previousOption () {
previousOption() {
const { folders } = this.props
let { optionIndex } = this.state
@@ -153,46 +171,52 @@ class FolderSelect extends React.Component {
})
}
selectOption () {
selectOption() {
const { folders } = this.props
const optionIndex = this.state.optionIndex
const folder = folders[optionIndex]
if (folder != null) {
this.setState({
status: 'FOCUS'
}, () => {
this.setValue(folder.key)
this.refs.root.focus()
})
this.setState(
{
status: 'FOCUS'
},
() => {
this.setValue(folder.key)
this.refs.root.focus()
}
)
}
}
handleOptionClick (storageKey, folderKey) {
return (e) => {
handleOptionClick(storageKey, folderKey) {
return e => {
e.stopPropagation()
this.setState({
status: 'FOCUS'
}, () => {
this.setValue(storageKey + '-' + folderKey)
this.refs.root.focus()
})
this.setState(
{
status: 'FOCUS'
},
() => {
this.setValue(storageKey + '-' + folderKey)
this.refs.root.focus()
}
)
}
}
setValue (value) {
setValue(value) {
this.value = value
this.props.onChange()
}
render () {
render() {
const { className, data, value } = this.props
const splitted = value.split('-')
const storageKey = splitted.shift()
const folderKey = splitted.shift()
let options = []
data.storageMap.forEach((storage, index) => {
storage.folders.forEach((folder) => {
storage.folders.forEach(folder => {
options.push({
storage: storage,
folder: folder
@@ -200,68 +224,78 @@ 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) {
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
.map((option, index) => {
return (
<div styleName={index === this.state.optionIndex
const optionList = options.map((option, index) => {
return (
<div
styleName={
index === this.state.optionIndex
? 'search-optionList-item--active'
: 'search-optionList-item'
}
key={option.storage.key + '-' + option.folder.key}
onClick={(e) => this.handleOptionClick(option.storage.key, option.folder.key)(e)}
}
key={option.storage.key + '-' + option.folder.key}
onClick={e =>
this.handleOptionClick(option.storage.key, option.folder.key)(e)
}
>
<span
styleName='search-optionList-item-name'
style={{ borderColor: option.folder.color }}
>
<span styleName='search-optionList-item-name'
style={{borderColor: option.folder.color}}
>
{option.folder.name}
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span>
{option.folder.name}
<span styleName='search-optionList-item-name-surfix'>
in {option.storage.name}
</span>
</div>
)
})
</span>
</div>
)
})
return (
<div className={_.isString(className)
? 'FolderSelect ' + className
: 'FolderSelect'
<div
className={
_.isString(className) ? 'FolderSelect ' + className : 'FolderSelect'
}
styleName={this.state.status === 'SEARCH'
? 'root--search'
: this.state.status === 'FOCUS'
? 'root--focus'
: 'root'
styleName={
this.state.status === 'SEARCH'
? 'root--search'
: this.state.status === 'FOCUS'
? 'root--focus'
: 'root'
}
ref='root'
tabIndex='0'
onClick={(e) => this.handleClick(e)}
onFocus={(e) => this.handleFocus(e)}
onBlur={(e) => this.handleBlur(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
onClick={e => this.handleClick(e)}
onFocus={e => this.handleFocus(e)}
onBlur={e => this.handleBlur(e)}
onKeyDown={e => this.handleKeyDown(e)}
>
{this.state.status === 'SEARCH'
? <div styleName='search'>
<input styleName='search-input'
{this.state.status === 'SEARCH' ? (
<div styleName='search'>
<input
styleName='search-input'
ref='search'
value={this.state.search}
placeholder={i18n.__('Folder...')}
onChange={(e) => this.handleSearchInputChange(e)}
onBlur={(e) => this.handleSearchInputBlur(e)}
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
onChange={e => this.handleSearchInputChange(e)}
onBlur={e => this.handleSearchInputBlur(e)}
onKeyDown={e => this.handleSearchInputKeyDown(e)}
/>
<div styleName='search-optionList'
ref='optionList'
>
<div styleName='search-optionList' ref='optionList'>
{optionList}
</div>
</div>
: <div styleName='idle' style={{color: currentOption.folder.color}}>
) : (
<div styleName='idle' style={{ color: currentOption.folder.color }}>
<div styleName='idle-label'>
<i className='fa fa-folder' />
<span styleName='idle-label-name'>
@@ -269,8 +303,7 @@ class FolderSelect extends React.Component {
</span>
</div>
</div>
}
)}
</div>
)
}
@@ -280,11 +313,13 @@ FolderSelect.propTypes = {
className: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.string,
folders: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
name: PropTypes.string,
color: PropTypes.string
}))
folders: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string,
name: PropTypes.string,
color: PropTypes.string
})
)
}
export default CSSModules(FolderSelect, styles)

View File

@@ -6,7 +6,7 @@ import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class FromUrlButton extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -14,44 +14,46 @@ class FromUrlButton extends React.Component {
}
}
handleMouseDown (e) {
handleMouseDown(e) {
this.setState({
isActive: true
})
}
handleMouseUp (e) {
handleMouseUp(e) {
this.setState({
isActive: false
})
}
handleMouseLeave (e) {
handleMouseLeave(e) {
this.setState({
isActive: false
})
}
render () {
render() {
const { className } = this.props
return (
<button className={_.isString(className)
? 'FromUrlButton ' + className
: 'FromUrlButton'
<button
className={
_.isString(className) ? 'FromUrlButton ' + className : 'FromUrlButton'
}
styleName={this.state.isActive || this.props.isActive
? 'root--active'
: 'root'
styleName={
this.state.isActive || this.props.isActive ? 'root--active' : 'root'
}
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}>
<img styleName='icon'
src={this.state.isActive || this.props.isActive
? '../resources/icon/icon-external.svg'
: '../resources/icon/icon-external.svg'
onMouseDown={e => this.handleMouseDown(e)}
onMouseUp={e => this.handleMouseUp(e)}
onMouseLeave={e => this.handleMouseLeave(e)}
onClick={this.props.onClick}
>
<img
styleName='icon'
src={
this.state.isActive || this.props.isActive
? '../resources/icon/icon-external.svg'
: '../resources/icon/icon-external.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Convert URL to Markdown')}</span>

View File

@@ -5,14 +5,18 @@ import styles from './FullscreenButton.styl'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
const FullscreenButton = ({
onClick
}) => {
const FullscreenButton = ({ onClick }) => {
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
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' />
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Fullscreen')}({hotkey})
</span>
</button>
)
}

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoButton.styl'
import i18n from 'browser/lib/i18n'
const InfoButton = ({
onClick
}) => (
<button styleName='control-infoButton'
onClick={(e) => onClick(e)}
>
const InfoButton = ({ onClick }) => (
<button styleName='control-infoButton' onClick={e => onClick(e)}>
<img className='infoButton' src='../resources/icon/icon-info.svg' />
<span styleName='tooltip'>{i18n.__('Info')}</span>
</button>

View File

@@ -6,28 +6,47 @@ import copy from 'copy-to-clipboard'
import i18n from 'browser/lib/i18n'
class InfoPanel extends React.Component {
copyNoteLink () {
const {noteLink} = this.props
copyNoteLink() {
const { noteLink } = this.props
this.refs.noteLink.select()
copy(noteLink)
}
render () {
render() {
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
return (
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
<div
className='infoPanel'
styleName='control-infoButton-panel'
style={{ display: 'none' }}
>
<div>
<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>
<hr />
{type === 'SNIPPET_NOTE'
? ''
: <div styleName='count-wrap'>
{type === 'SNIPPET_NOTE' ? (
''
) : (
<div styleName='count-wrap'>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{wordCount}</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>
</div>
</div>
}
)}
{type === 'SNIPPET_NOTE'
? ''
: <hr />
}
{type === 'SNIPPET_NOTE' ? '' : <hr />}
<div>
<p styleName='infoPanel-default'>{storageName}</p>
@@ -60,8 +76,18 @@ class InfoPanel extends React.Component {
</div>
<div>
<input styleName='infoPanel-noteLink' ref='noteLink' defaultValue={noteLink} onClick={(e) => { e.target.select() }} />
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
<input
styleName='infoPanel-noteLink'
ref='noteLink'
defaultValue={noteLink}
onClick={e => {
e.target.select()
}}
/>
<button
onClick={() => this.copyNoteLink()}
styleName='infoPanel-copyButton'
>
<i className='fa fa-clipboard' />
</button>
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
@@ -70,27 +96,39 @@ class InfoPanel extends React.Component {
<hr />
<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' />
<p>{i18n.__('.md')}</p>
</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' />
<p>{i18n.__('.txt')}</p>
</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' />
<p>{i18n.__('.html')}</p>
</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' />
<p>{i18n.__('.pdf')}</p>
</button>
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
<button styleName='export--enable' onClick={e => print(e, 'print')}>
<i className='fa fa-print' />
<p>{i18n.__('Print')}</p>
</button>

View File

@@ -5,9 +5,20 @@ import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n'
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>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
@@ -21,7 +32,10 @@ const InfoPanelTrashed = ({
</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>
</div>
@@ -31,22 +45,34 @@ const InfoPanelTrashed = ({
</div>
<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' />
<p>.md</p>
</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' />
<p>.txt</p>
</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' />
<p>.html</p>
</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' />
<p>.pdf</p>
</button>

View File

@@ -34,16 +34,19 @@ import { replace } from 'connected-react-router'
import ToggleDirectionButton from 'browser/main/Detail/ToggleDirectionButton'
class MarkdownNoteDetail extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
isMovingNote: false,
note: Object.assign({
title: '',
content: '',
linesHighlighted: []
}, props.note),
note: Object.assign(
{
title: '',
content: '',
linesHighlighted: []
},
props.note
),
isLockButtonShown: props.config.editor.type !== 'SPLIT',
isLocked: false,
editorType: props.config.editor.type,
@@ -57,37 +60,42 @@ class MarkdownNoteDetail extends React.Component {
this.generateToc = () => this.handleGenerateToc()
}
focus () {
focus() {
this.refs.content.focus()
}
componentDidMount () {
componentDidMount() {
ee.on('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection())
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)
})
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
ee.on('code:generate-toc', this.generateToc)
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
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.saveQueue != null) this.saveNow()
this.setState({
note: Object.assign({linesHighlighted: []}, nextProps.note)
}, () => {
this.refs.content.reload()
if (this.refs.tags) this.refs.tags.reset()
})
this.setState(
{
note: Object.assign({ linesHighlighted: [] }, nextProps.note)
},
() => {
this.refs.content.reload()
if (this.refs.tags) this.refs.tags.reset()
}
)
}
// Focus content if using blur or double click
// --> Moved here from componentDidMount so a re-render during search won't set focus to the editor
const {switchPreview} = nextProps.config.editor
const { switchPreview } = nextProps.config.editor
if (this.state.switchPreview !== switchPreview) {
this.setState({
@@ -100,24 +108,28 @@ class MarkdownNoteDetail extends React.Component {
}
}
componentWillUnmount () {
componentWillUnmount() {
ee.off('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', this.handleSwitchDirection)
ee.off('code:generate-toc', this.generateToc)
if (this.saveQueue != null) this.saveNow()
}
handleUpdateTag () {
handleUpdateTag() {
const { note } = this.state
if (this.refs.tags) note.tags = this.refs.tags.value
this.updateNote(note)
}
handleUpdateContent () {
handleUpdateContent() {
const { note } = this.state
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 = markdown.strip(title)
note.title = title
@@ -125,37 +137,35 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note)
}
updateNote (note) {
updateNote(note) {
note.updatedAt = new Date()
this.setState({note}, () => {
this.setState({ note }, () => {
this.save()
})
}
save () {
save() {
clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => {
this.saveNow()
}, 1000)
}
saveNow () {
saveNow() {
const { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = null
dataApi
.updateNote(note.storage, note.key, this.state.note)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
dataApi.updateNote(note.storage, note.key, this.state.note).then(note => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
})
}
handleFolderChange (e) {
handleFolderChange(e) {
const { note } = this.state
const value = this.refs.folder.value
const splitted = value.split('-')
@@ -164,64 +174,71 @@ class MarkdownNoteDetail extends React.Component {
dataApi
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
const { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
originNote: note,
note: newNote
})
dispatch(replace({
pathname: location.pathname,
search: queryString.stringify({
key: newNote.key
.then(newNote => {
this.setState(
{
isMovingNote: true,
note: Object.assign({}, newNote)
},
() => {
const { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
originNote: note,
note: newNote
})
}))
this.setState({
isMovingNote: false
})
})
dispatch(
replace({
pathname: location.pathname,
search: queryString.stringify({
key: newNote.key
})
})
)
this.setState({
isMovingNote: false
})
}
)
})
}
handleStarButtonClick (e) {
handleStarButtonClick(e) {
const { note } = this.state
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
if (!note.isStarred)
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
note.isStarred = !note.isStarred
this.setState({
note
}, () => {
this.save()
})
this.setState(
{
note
},
() => {
this.save()
}
)
}
exportAsFile () {
exportAsFile() {}
}
exportAsMd () {
exportAsMd() {
ee.emit('export:save-md')
}
exportAsTxt () {
exportAsTxt() {
ee.emit('export:save-text')
}
exportAsHtml () {
exportAsHtml() {
ee.emit('export:save-html')
}
exportAsPdf () {
exportAsPdf() {
ee.emit('export:save-pdf')
}
handleKeyDown (e) {
handleKeyDown(e) {
switch (e.keyCode) {
// tab key
case 9:
@@ -231,7 +248,11 @@ class MarkdownNoteDetail extends React.Component {
} else if (e.ctrlKey && e.shiftKey) {
e.preventDefault()
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()
this.focusEditor()
}
@@ -239,9 +260,8 @@ class MarkdownNoteDetail extends React.Component {
// I key
case 73:
{
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
const isSuper =
global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey
if (isSuper) {
e.preventDefault()
this.handleInfoButtonClick(e)
@@ -251,17 +271,17 @@ class MarkdownNoteDetail extends React.Component {
}
}
handleTrashButtonClick (e) {
handleTrashButtonClick(e) {
const { note } = this.state
const { isTrashed } = note
const { confirmDeletion } = this.props.config.ui
if (isTrashed) {
if (confirmDeleteNote(confirmDeletion, true)) {
const {note, dispatch} = this.props
const { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
.then(data => {
const dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
@@ -277,109 +297,124 @@ class MarkdownNoteDetail extends React.Component {
if (confirmDeleteNote(confirmDeletion, false)) {
note.isTrashed = true
this.setState({
note
}, () => {
this.save()
})
this.setState(
{
note
},
() => {
this.save()
}
)
ee.emit('list:next')
}
}
}
handleUndoButtonClick (e) {
handleUndoButtonClick(e) {
const { note } = this.state
note.isTrashed = false
this.setState({
note
}, () => {
this.save()
this.refs.content.reload()
ee.emit('list:next')
})
this.setState(
{
note
},
() => {
this.save()
this.refs.content.reload()
ee.emit('list:next')
}
)
}
handleFullScreenButton (e) {
handleFullScreenButton(e) {
ee.emit('editor:fullscreen')
}
handleLockButtonMouseDown (e) {
handleLockButtonMouseDown(e) {
e.preventDefault()
ee.emit('editor:lock')
this.setState({ isLocked: !this.state.isLocked })
if (this.state.isLocked) this.focus()
}
getToggleLockButton () {
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
getToggleLockButton() {
return this.state.isLocked
? '../resources/icon/icon-lock.svg'
: '../resources/icon/icon-unlock.svg'
}
handleDeleteKeyDown (e) {
handleDeleteKeyDown(e) {
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
}
handleToggleLockButton (event, noteStatus) {
handleToggleLockButton(event, noteStatus) {
// first argument event is not used
if (noteStatus === 'CODE') {
this.setState({isLockButtonShown: true})
this.setState({ isLockButtonShown: true })
} else {
this.setState({isLockButtonShown: false})
this.setState({ isLockButtonShown: false })
}
}
handleGenerateToc () {
handleGenerateToc() {
const editor = this.refs.content.refs.code.editor
markdownToc.generateInEditor(editor)
}
handleFocus (e) {
handleFocus(e) {
this.focus()
}
handleInfoButtonClick (e) {
handleInfoButtonClick(e) {
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) {
ee.emit('print')
}
handleSwitchMode (type) {
handleSwitchMode(type) {
// If in split mode, hide the lock button
this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type
ConfigManager.set(newConfig)
})
this.setState(
{ editorType: type, isLockButtonShown: !(type === 'SPLIT') },
() => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type
ConfigManager.set(newConfig)
}
)
}
handleSwitchDirection () {
handleSwitchDirection() {
// If in split mode, hide the lock button
const direction = this.state.RTL
this.setState({ RTL: !direction })
}
handleDeleteNote () {
handleDeleteNote() {
this.handleTrashButtonClick()
}
handleClearTodo () {
handleClearTodo() {
const { note } = this.state
const splitted = note.content.split('\n')
const clearTodoContent = splitted.map((line) => {
const trimmedLine = line.trim()
if (trimmedLine.match(/\[x\]/i)) {
return line.replace(/\[x\]/i, '[ ]')
} else {
return line
}
}).join('\n')
const clearTodoContent = splitted
.map(line => {
const trimmedLine = line.trim()
if (trimmedLine.match(/\[x\]/i)) {
return line.replace(/\[x\]/i, '[ ]')
} else {
return line
}
})
.join('\n')
note.content = clearTodoContent
this.refs.content.setValue(note.content)
@@ -387,40 +422,44 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note)
}
renderEditor () {
renderEditor() {
const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state
if (this.state.editorType === 'EDITOR_PREVIEW') {
return <MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={this.state.RTL}
/>
return (
<MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={this.state.RTL}
/>
)
} else {
return <MarkdownSplitEditor
ref='content'
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={this.state.RTL}
/>
return (
<MarkdownSplitEditor
ref='content'
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={this.state.RTL}
/>
)
}
}
render () {
render() {
const { data, dispatch, location, config } = this.props
const { note, editorType } = this.state
const storageKey = note.storage
@@ -428,123 +467,141 @@ class MarkdownNoteDetail extends React.Component {
const options = []
data.storageMap.forEach((storage, index) => {
storage.folders.forEach((folder) => {
storage.folders.forEach(folder => {
options.push({
storage: storage,
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'>
<div styleName='info-left'>
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
</div>
<div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanelTrashed
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsHtml={this.exportAsHtml}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsPdf={this.exportAsPdf}
/>
</div>
</div>
const detailTopBar = <div styleName='info'>
<div styleName='info-left'>
<div>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
data={data}
onChange={(e) => this.handleFolderChange(e)}
const trashTopBar = (
<div styleName='info'>
<div styleName='info-left'>
<RestoreButton onClick={e => this.handleUndoButtonClick(e)} />
</div>
<div styleName='info-right'>
<PermanentDeleteButton
onClick={e => this.handleTrashButtonClick(e)}
/>
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanelTrashed
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsHtml={this.exportAsHtml}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsPdf={this.exportAsPdf}
/>
</div>
<TagSelect
ref='tags'
value={this.state.note.tags}
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
dispatch={dispatch}
onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags}
/>
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div>
<div styleName='info-right'>
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<ToggleDirectionButton onClick={(e) => this.handleSwitchDirection(e)} isRTL={this.state.RTL} />
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
)
{(() => {
const imgSrc = `${this.getToggleLockButton()}`
const lockButtonComponent =
<button styleName='control-lockButton'
onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<img src={imgSrc} />
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
</button>
const detailTopBar = (
<div styleName='info'>
<div styleName='info-left'>
<div>
<FolderSelect
styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
data={data}
onChange={e => this.handleFolderChange(e)}
/>
</div>
return (
this.state.isLockButtonShown ? lockButtonComponent : ''
)
})()}
<TagSelect
ref='tags'
value={this.state.note.tags}
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
dispatch={dispatch}
onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags}
/>
<TodoListPercentage
onClearCheckboxClick={e => this.handleClearTodo(e)}
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
/>
</div>
<div styleName='info-right'>
<ToggleModeButton
onClick={e => this.handleSwitchMode(e)}
editorType={editorType}
/>
<ToggleDirectionButton
onClick={e => this.handleSwitchDirection(e)}
isRTL={this.state.RTL}
/>
<StarButton
onClick={e => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
{(() => {
const imgSrc = `${this.getToggleLockButton()}`
const lockButtonComponent = (
<button
styleName='control-lockButton'
onFocus={e => this.handleFocus(e)}
onMouseDown={e => this.handleLockButtonMouseDown(e)}
>
<img src={imgSrc} />
{this.state.isLocked ? (
<span styleName='tooltip'>Unlock</span>
) : (
<span styleName='tooltip'>Lock</span>
)}
</button>
)
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
return this.state.isLockButtonShown ? lockButtonComponent : ''
})()}
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<FullscreenButton onClick={e => this.handleFullScreenButton(e)} />
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml}
exportAsPdf={this.exportAsPdf}
wordCount={note.content.trim().split(/\s+/g).length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
print={this.print}
/>
<TrashButton onClick={e => this.handleTrashButtonClick(e)} />
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](:note:${
queryString.parse(location.search).key
})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml}
exportAsPdf={this.exportAsPdf}
wordCount={note.content.trim().split(/\s+/g).length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
print={this.print}
/>
</div>
</div>
</div>
)
return (
<div className='NoteDetail'
<div
className='NoteDetail'
style={this.props.style}
styleName='root'
onKeyDown={(e) => this.handleKeyDown(e)}
onKeyDown={e => this.handleKeyDown(e)}
>
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
<div styleName='body'>
{this.renderEditor()}
</div>
<div styleName='body'>{this.renderEditor()}</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
@@ -558,9 +615,7 @@ class MarkdownNoteDetail extends React.Component {
MarkdownNoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
note: PropTypes.shape({
}),
note: PropTypes.shape({}),
style: PropTypes.shape({
left: PropTypes.number
}),

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const PermanentDeleteButton = ({
onClick
}) => (
<button styleName='control-trashButton--in-trash'
onClick={(e) => onClick(e)}
>
const PermanentDeleteButton = ({ onClick }) => (
<button styleName='control-trashButton--in-trash' onClick={e => onClick(e)}>
<img src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button>

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './RestoreButton.styl'
import i18n from 'browser/lib/i18n'
const RestoreButton = ({
onClick
}) => (
<button styleName='control-restoreButton'
onClick={onClick}
>
const RestoreButton = ({ onClick }) => (
<button styleName='control-restoreButton' onClick={onClick}>
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
<span styleName='tooltip'>{i18n.__('Restore')}</span>
</button>

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class StarButton extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -14,47 +14,51 @@ class StarButton extends React.Component {
}
}
handleMouseDown (e) {
handleMouseDown(e) {
this.setState({
isActive: true
})
}
handleMouseUp (e) {
handleMouseUp(e) {
this.setState({
isActive: false
})
}
handleMouseLeave (e) {
handleMouseLeave(e) {
this.setState({
isActive: false
})
}
render () {
render() {
const { className } = this.props
return (
<button className={_.isString(className)
? 'StarButton ' + className
: 'StarButton'
<button
className={
_.isString(className) ? 'StarButton ' + className : 'StarButton'
}
styleName={this.state.isActive || this.props.isActive
? 'root--active'
: 'root'
styleName={
this.state.isActive || this.props.isActive ? 'root--active' : 'root'
}
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}>
<img styleName='icon'
src={this.state.isActive || this.props.isActive
? '../resources/icon/icon-starred.svg'
: '../resources/icon/icon-star.svg'
onMouseDown={e => this.handleMouseDown(e)}
onMouseUp={e => this.handleMouseUp(e)}
onMouseLeave={e => this.handleMouseLeave(e)}
onClick={this.props.onClick}
>
<img
styleName='icon'
src={
this.state.isActive || this.props.isActive
? '../resources/icon/icon-starred.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>
)
}

View File

@@ -11,7 +11,7 @@ import Autosuggest from 'react-autosuggest'
import { push } from 'connected-react-router'
class TagSelect extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -23,12 +23,16 @@ class TagSelect extends React.Component {
this.onInputBlur = this.onInputBlur.bind(this)
this.onInputChange = this.onInputChange.bind(this)
this.onInputKeyDown = this.onInputKeyDown.bind(this)
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(
this
)
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(
this
)
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
}
addNewTag (newTag) {
addNewTag(newTag) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
newTag = newTag.trim().replace(/ +/g, '_')
@@ -44,9 +48,7 @@ class TagSelect extends React.Component {
}
let { value } = this.props
value = _.isArray(value)
? value.slice()
: []
value = _.isArray(value) ? value.slice() : []
if (!_.includes(value, newTag)) {
value.push(newTag)
@@ -56,27 +58,31 @@ class TagSelect extends React.Component {
value = _.sortBy(value)
}
this.setState({
newTag: ''
}, () => {
this.value = value
this.props.onChange()
})
this.setState(
{
newTag: ''
},
() => {
this.value = value
this.props.onChange()
}
)
}
buildSuggestions () {
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
(tag, name) => ({
name,
nameLC: name.toLowerCase(),
size: tag.size
})
).filter(
tag => tag.size > 0
), ['name'])
buildSuggestions() {
this.suggestions = _.sortBy(
this.props.data.tagNoteMap
.map((tag, name) => ({
name,
nameLC: name.toLowerCase(),
size: tag.size
}))
.filter(tag => tag.size > 0),
['name']
)
}
componentDidMount () {
componentDidMount() {
this.value = this.props.value
this.buildSuggestions()
@@ -84,19 +90,19 @@ class TagSelect extends React.Component {
ee.on('editor:add-tag', this.handleAddTag)
}
componentDidUpdate () {
componentDidUpdate() {
this.value = this.props.value
}
componentWillUnmount () {
componentWillUnmount() {
ee.off('editor:add-tag', this.handleAddTag)
}
handleAddTag () {
handleAddTag() {
this.refs.newTag.input.focus()
}
handleTagLabelClick (tag) {
handleTagLabelClick(tag) {
const { dispatch } = this.props
// Note: `tag` requires encoding later.
@@ -104,23 +110,23 @@ class TagSelect extends React.Component {
dispatch(push(`/tags/${tag}`))
}
handleTagRemoveButtonClick (tag) {
handleTagRemoveButtonClick(tag) {
this.removeTagByCallback((value, tag) => {
value.splice(value.indexOf(tag), 1)
}, tag)
}
onInputBlur (e) {
onInputBlur(e) {
this.submitNewTag()
}
onInputChange (e, { newValue, method }) {
onInputChange(e, { newValue, method }) {
this.setState({
newTag: newValue
})
}
onInputKeyDown (e) {
onInputKeyDown(e) {
switch (e.keyCode) {
case 9:
e.preventDefault()
@@ -136,17 +142,18 @@ class TagSelect extends React.Component {
}
}
onSuggestionsClearRequested () {
onSuggestionsClearRequested() {
this.setState({
suggestions: []
})
}
onSuggestionsFetchRequested ({ value }) {
onSuggestionsFetchRequested({ value }) {
const valueLC = value.toLowerCase()
const suggestions = _.filter(
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({
@@ -154,22 +161,20 @@ class TagSelect extends React.Component {
})
}
onSuggestionSelected (event, { suggestion, suggestionValue }) {
onSuggestionSelected(event, { suggestion, suggestionValue }) {
this.addNewTag(suggestionValue)
}
removeLastTag () {
this.removeTagByCallback((value) => {
removeLastTag() {
this.removeTagByCallback(value => {
value.pop()
})
}
removeTagByCallback (callback, tag = null) {
removeTagByCallback(callback, tag = null) {
let { value } = this.props
value = _.isArray(value)
? value.slice()
: []
value = _.isArray(value) ? value.slice() : []
callback(value, tag)
value = _.uniq(value)
@@ -177,7 +182,7 @@ class TagSelect extends React.Component {
this.props.onChange()
}
reset () {
reset() {
this.buildSuggestions()
this.setState({
@@ -185,51 +190,60 @@ class TagSelect extends React.Component {
})
}
submitNewTag () {
submitNewTag() {
this.addNewTag(this.refs.newTag.input.value)
}
render () {
render() {
const { value, className, showTagsAlphabetically, coloredTags } = this.props
const tagList = _.isArray(value)
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
const wrapperStyle = {}
const textStyle = {}
const BLACK = '#333333'
const WHITE = '#f1f1f1'
const color = coloredTags[tag]
const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE })
let iconRemove = '../resources/icon/icon-x.svg'
if (color) {
wrapperStyle.backgroundColor = color
textStyle.color = invertedColor
}
if (invertedColor === WHITE) {
iconRemove = '../resources/icon/icon-x-light.svg'
}
return (
<span styleName='tag'
key={tag}
style={wrapperStyle}
>
<span styleName='tag-label' style={textStyle} onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
<button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
>
<img className='tag-removeButton-icon' src={iconRemove} width='8px' />
</button>
</span>
)
})
? (showTagsAlphabetically ? _.sortBy(value) : value).map(tag => {
const wrapperStyle = {}
const textStyle = {}
const BLACK = '#333333'
const WHITE = '#f1f1f1'
const color = coloredTags[tag]
const invertedColor =
color && invertColor(color, { black: BLACK, white: WHITE })
let iconRemove = '../resources/icon/icon-x.svg'
if (color) {
wrapperStyle.backgroundColor = color
textStyle.color = invertedColor
}
if (invertedColor === WHITE) {
iconRemove = '../resources/icon/icon-x-light.svg'
}
return (
<span styleName='tag' key={tag} style={wrapperStyle}>
<span
styleName='tag-label'
style={textStyle}
onClick={e => this.handleTagLabelClick(tag)}
>
#{tag}
</span>
<button
styleName='tag-removeButton'
onClick={e => this.handleTagRemoveButtonClick(tag)}
>
<img
className='tag-removeButton-icon'
src={iconRemove}
width='8px'
/>
</button>
</span>
)
})
: []
const { newTag, suggestions } = this.state
return (
<div className={_.isString(className)
? 'TagSelect ' + className
: 'TagSelect'
<div
className={
_.isString(className) ? 'TagSelect ' + className : 'TagSelect'
}
styleName='root'
>
@@ -241,11 +255,7 @@ class TagSelect extends React.Component {
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
getSuggestionValue={suggestion => suggestion.name}
renderSuggestion={suggestion => (
<div>
{suggestion.name}
</div>
)}
renderSuggestion={suggestion => <div>{suggestion.name}</div>}
inputProps={{
placeholder: i18n.__('Add tag...'),
value: newTag,

View File

@@ -4,9 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleDirectionButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleDirectionButton = ({
onClick, isRTL
}) => (
const ToggleDirectionButton = ({ onClick, isRTL }) => (
<div styleName='control-toggleModeButton'>
<div styleName={isRTL ? 'active' : undefined} onClick={() => onClick()}>
<img src={!isRTL ? '../resources/icon/icon-left-to-right.svg' : ''} />
@@ -14,7 +12,9 @@ const ToggleDirectionButton = ({
<div styleName={!isRTL ? 'active' : undefined} onClick={() => onClick()}>
<img src={!isRTL ? '' : '../resources/icon/icon-right-to-left.svg'} />
</div>
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Direction')}</span>
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Toggle Direction')}
</span>
</div>
)

View File

@@ -4,17 +4,35 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleModeButton = ({
onClick, editorType
}) => (
const ToggleModeButton = ({ onClick, editorType }) => (
<div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : undefined} onClick={() => onClick('SPLIT')}>
<img src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
<div
styleName={editorType === 'SPLIT' ? 'active' : undefined}
onClick={() => onClick('SPLIT')}
>
<img
src={
editorType === 'EDITOR_PREVIEW'
? '../resources/icon/icon-mode-markdown-off-active.svg'
: ''
}
/>
</div>
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined} onClick={() => onClick('EDITOR_PREVIEW')}>
<img src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
<div
styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined}
onClick={() => onClick('EDITOR_PREVIEW')}
>
<img
src={
editorType === 'EDITOR_PREVIEW'
? ''
: '../resources/icon/icon-mode-split-on-active.svg'
}
/>
</div>
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Toggle Mode')}
</span>
</div>
)

View File

@@ -4,14 +4,12 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const TrashButton = ({
onClick
}) => (
<button styleName='control-trashButton'
onClick={(e) => onClick(e)}
>
const TrashButton = ({ onClick }) => (
<button styleName='control-trashButton' onClick={e => onClick(e)}>
<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>
)

View File

@@ -15,7 +15,7 @@ import queryString from 'query-string'
const OSX = global.process.platform === 'darwin'
class Detail extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.focusHandler = () => {
@@ -26,41 +26,55 @@ class Detail extends React.Component {
}
}
componentDidMount () {
componentDidMount() {
ee.on('detail:focus', this.focusHandler)
ee.on('detail:delete', this.deleteHandler)
}
componentWillUnmount () {
componentWillUnmount() {
ee.off('detail:focus', this.focusHandler)
ee.off('detail:delete', this.deleteHandler)
}
render () {
const { location, data, match: { params }, config } = this.props
const noteKey = location.search !== '' && queryString.parse(location.search).key
render() {
const {
location,
data,
match: { params },
config
} = this.props
const noteKey =
location.search !== '' && queryString.parse(location.search).key
let note = null
if (location.search !== '') {
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
if (location.pathname.match(/\/searched/)) {
const searchStr = params.searchword
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
: searchFromNotes(allNotes, searchStr)
displayedNotes =
searchStr === undefined || searchStr === ''
? allNotes
: searchFromNotes(allNotes, searchStr)
} else if (location.pathname.match(/^\/tags/)) {
const listOfTags = params.tagname.split(' ')
displayedNotes = data.noteMap.map(note => note).filter(note =>
listOfTags.every(tag => note.tags.includes(tag))
)
displayedNotes = data.noteMap
.map(note => note)
.filter(note => listOfTags.every(tag => note.tags.includes(tag)))
}
if (location.pathname.match(/^\/trashed/)) {
displayedNotes = trashedNotes
} 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)
@@ -71,12 +85,12 @@ class Detail extends React.Component {
if (note == null) {
return (
<div styleName='root'
style={this.props.style}
tabIndex='0'
>
<div styleName='root' style={this.props.style} tabIndex='0'>
<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>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}

View File

@@ -24,7 +24,7 @@ const electron = require('electron')
const { remote } = electron
class Main extends React.Component {
constructor (props) {
constructor(props) {
super(props)
if (process.env.NODE_ENV === 'production') {
@@ -46,7 +46,7 @@ class Main extends React.Component {
this.toggleFullScreen = () => this.handleFullScreenButton()
}
getChildContext () {
getChildContext() {
const { status, config } = this.props
return {
@@ -55,7 +55,7 @@ class Main extends React.Component {
}
}
init () {
init() {
dataApi
.addStorage({
name: 'My Storage Location',
@@ -93,18 +93,21 @@ class Main extends React.Component {
type: 'SNIPPET_NOTE',
folder: data.storage.folders[0].key,
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: [
{
name: 'example.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: []
},
{
name: 'example.js',
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: []
}
]
@@ -120,7 +123,8 @@ class Main extends React.Component {
type: 'MARKDOWN_NOTE',
folder: data.storage.folders[0].key,
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 => {
store.dispatch({
@@ -141,7 +145,7 @@ class Main extends React.Component {
})
}
componentDidMount () {
componentDidMount() {
const { dispatch, config } = this.props
if (uiThemes.some(theme => theme.name === config.ui.theme)) {
@@ -173,38 +177,44 @@ class Main extends React.Component {
delete CodeMirror.keyMap.emacs['Ctrl-V']
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('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
eventEmitter.off(
'menubar:togglemenubar',
this.toggleMenuBarVisible.bind(this)
)
}
toggleMenuBarVisible () {
toggleMenuBarVisible() {
const { config } = this.props
const { ui } = config
const newUI = Object.assign(ui, {showMenuBar: !ui.showMenuBar})
const newUI = Object.assign(ui, { showMenuBar: !ui.showMenuBar })
const newConfig = Object.assign(config, newUI)
ConfigManager.set(newConfig)
}
handleLeftSlideMouseDown (e) {
handleLeftSlideMouseDown(e) {
e.preventDefault()
this.setState({
isLeftSliderFocused: true
})
}
handleRightSlideMouseDown (e) {
handleRightSlideMouseDown(e) {
e.preventDefault()
this.setState({
isRightSliderFocused: true
})
}
handleMouseUp (e) {
handleMouseUp(e) {
// Change width of NoteList component.
if (this.state.isRightSliderFocused) {
this.setState(
@@ -244,7 +254,7 @@ class Main extends React.Component {
}
}
handleMouseMove (e) {
handleMouseMove(e) {
if (this.state.isRightSliderFocused) {
const offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
@@ -270,7 +280,7 @@ class Main extends React.Component {
}
}
handleFullScreenButton (e) {
handleFullScreenButton(e) {
this.setState({ fullScreen: !this.state.fullScreen }, () => {
const noteDetail = document.querySelector('.NoteDetail')
const noteList = document.querySelector('.NoteList')
@@ -284,7 +294,7 @@ class Main extends React.Component {
})
}
hideLeftLists (noteDetail, noteList, mainBody) {
hideLeftLists(noteDetail, noteList, mainBody) {
this.setState({ noteDetailWidth: noteDetail.style.left })
this.setState({ mainBodyWidth: mainBody.style.left })
noteDetail.style.left = '0px'
@@ -292,13 +302,13 @@ class Main extends React.Component {
noteList.style.display = 'none'
}
showLeftLists (noteDetail, noteList, mainBody) {
showLeftLists(noteDetail, noteList, mainBody) {
noteDetail.style.left = this.state.noteDetailWidth
mainBody.style.left = this.state.mainBodyWidth
noteList.style.display = 'inline'
}
render () {
render() {
const { config } = this.props
// the width of the navigation bar when it is folded/collapsed
@@ -312,10 +322,16 @@ class Main extends React.Component {
onMouseUp={e => this.handleMouseUp(e)}
>
<SideNav
{..._.pick(this.props, ['dispatch', 'data', 'config', 'match', 'location'])}
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'match',
'location'
])}
width={this.state.navWidth}
/>
{!config.isSideNavFolded &&
{!config.isSideNavFolded && (
<div
styleName={
this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
@@ -325,7 +341,8 @@ class Main extends React.Component {
draggable='false'
>
<div styleName='slider-hitbox' />
</div>}
</div>
)}
<div
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
id='main-body'

View File

@@ -15,30 +15,48 @@ const { dialog } = remote
const OSX = window.process.platform === 'darwin'
class NewNoteButton extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
}
this.state = {}
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
}
componentDidMount () {
componentDidMount() {
eventEmitter.on('top:new-note', this.handleNewNoteButtonClick)
}
componentWillUnmount () {
componentWillUnmount() {
eventEmitter.off('top:new-note', this.handleNewNoteButtonClick)
}
handleNewNoteButtonClick (e) {
const { location, dispatch, match: { params }, config } = this.props
handleNewNoteButtonClick(e) {
const {
location,
dispatch,
match: { params },
config
} = this.props
const { storage, folder } = this.resolveTargetFolder()
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
createMarkdownNote(
storage.key,
folder.key,
dispatch,
location,
params,
config
)
} 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 {
modal.open(NewNoteModal, {
storage: storage.key,
@@ -51,8 +69,11 @@ class NewNoteButton extends React.Component {
}
}
resolveTargetFolder () {
const { data, match: { params } } = this.props
resolveTargetFolder() {
const {
data,
match: { params }
} = this.props
let storage = data.storageMap.get(params.storageKey)
// Find first storage
if (storage == null) {
@@ -62,9 +83,12 @@ class NewNoteButton extends React.Component {
}
}
if (storage == null) this.showMessageBox(i18n.__('No storage 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'))
if (storage == null)
this.showMessageBox(i18n.__('No storage 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 {
storage,
@@ -72,7 +96,7 @@ class NewNoteButton extends React.Component {
}
}
showMessageBox (message) {
showMessageBox(message) {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: message,
@@ -80,16 +104,19 @@ class NewNoteButton extends React.Component {
})
}
render () {
render() {
const { config, style } = this.props
return (
<div className='NewNoteButton'
<div
className='NewNoteButton'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
style={style}
>
<div styleName='control'>
<button styleName='control-newNoteButton'
onClick={this.handleNewNoteButtonClick}>
<button
styleName='control-newNoteButton'
onClick={this.handleNewNoteButtonClick}
>
<img src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'>
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,17 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const ListButton = ({
onClick, isTagActive
}) => (
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={onClick}>
<img src={isTagActive
? '../resources/icon/icon-list.svg'
: '../resources/icon/icon-list-active.svg'
}
const ListButton = ({ onClick, isTagActive }) => (
<button
styleName={isTagActive ? 'non-active-button' : 'active-button'}
onClick={onClick}
>
<img
src={
isTagActive
? '../resources/icon/icon-list.svg'
: '../resources/icon/icon-list-active.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Notes')}</span>
</button>

View File

@@ -4,10 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferenceButton.styl'
import i18n from 'browser/lib/i18n'
const PreferenceButton = ({
onClick
}) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
const PreferenceButton = ({ onClick }) => (
<button styleName='top-menu-preference' onClick={e => onClick(e)}>
<img src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
</button>

View File

@@ -19,7 +19,7 @@ const escapeStringRegexp = require('escape-string-regexp')
const path = require('path')
class StorageItem extends React.Component {
constructor (props) {
constructor(props) {
super(props)
const { storage } = this.props
@@ -30,11 +30,11 @@ class StorageItem extends React.Component {
}
}
handleHeaderContextMenu (e) {
handleHeaderContextMenu(e) {
context.popup([
{
label: i18n.__('Add Folder'),
click: (e) => this.handleAddFolderButtonClick(e)
click: e => this.handleAddFolderButtonClick(e)
},
{
type: 'separator'
@@ -44,11 +44,11 @@ class StorageItem extends React.Component {
submenu: [
{
label: i18n.__('Export as txt'),
click: (e) => this.handleExportStorageClick(e, 'txt')
click: e => this.handleExportStorageClick(e, 'txt')
},
{
label: i18n.__('Export as md'),
click: (e) => this.handleExportStorageClick(e, 'md')
click: e => this.handleExportStorageClick(e, 'md')
}
]
},
@@ -57,75 +57,74 @@ class StorageItem extends React.Component {
},
{
label: i18n.__('Unlink Storage'),
click: (e) => this.handleUnlinkStorageClick(e)
click: e => this.handleUnlinkStorageClick(e)
}
])
}
handleUnlinkStorageClick (e) {
handleUnlinkStorageClick(e) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
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')]
})
if (index === 0) {
const { storage, dispatch } = this.props
dataApi.removeStorage(storage.key)
dataApi
.removeStorage(storage.key)
.then(() => {
dispatch({
type: 'REMOVE_STORAGE',
storageKey: storage.key
})
})
.catch((err) => {
.catch(err => {
throw err
})
}
}
handleExportStorageClick (e, fileType) {
handleExportStorageClick(e, fileType) {
const options = {
properties: ['openDirectory', 'createDirectory'],
buttonLabel: i18n.__('Select directory'),
title: i18n.__('Select a folder to export the files to'),
multiSelections: false
}
dialog.showOpenDialog(remote.getCurrentWindow(), options,
(paths) => {
if (paths && paths.length === 1) {
const { storage, dispatch } = this.props
dataApi
.exportStorage(storage.key, fileType, paths[0])
.then(data => {
dispatch({
type: 'EXPORT_STORAGE',
storage: data.storage,
fileType: data.fileType
})
})
}
})
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) {
const { storage, dispatch } = this.props
dataApi.exportStorage(storage.key, fileType, paths[0]).then(data => {
dispatch({
type: 'EXPORT_STORAGE',
storage: data.storage,
fileType: data.fileType
})
})
}
})
}
handleToggleButtonClick (e) {
handleToggleButtonClick(e) {
const { storage, dispatch } = this.props
const isOpen = !this.state.isOpen
dataApi.toggleStorage(storage.key, isOpen)
.then((storage) => {
dispatch({
type: 'EXPAND_STORAGE',
storage,
isOpen
})
dataApi.toggleStorage(storage.key, isOpen).then(storage => {
dispatch({
type: 'EXPAND_STORAGE',
storage,
isOpen
})
})
this.setState({
isOpen: isOpen
})
}
handleAddFolderButtonClick (e) {
handleAddFolderButtonClick(e) {
const { storage } = this.props
modal.open(CreateFolderModal, {
@@ -133,23 +132,23 @@ class StorageItem extends React.Component {
})
}
handleHeaderInfoClick (e) {
handleHeaderInfoClick(e) {
const { storage, dispatch } = this.props
dispatch(push('/storages/' + storage.key))
}
handleFolderButtonClick (folderKey) {
return (e) => {
handleFolderButtonClick(folderKey) {
return e => {
const { storage, dispatch } = this.props
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
}
}
handleFolderButtonContextMenu (e, folder) {
handleFolderButtonContextMenu(e, folder) {
context.popup([
{
label: i18n.__('Rename Folder'),
click: (e) => this.handleRenameFolderClick(e, folder)
click: e => this.handleRenameFolderClick(e, folder)
},
{
type: 'separator'
@@ -159,11 +158,11 @@ class StorageItem extends React.Component {
submenu: [
{
label: i18n.__('Export as txt'),
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
click: e => this.handleExportFolderClick(e, folder, 'txt')
},
{
label: i18n.__('Export as md'),
click: (e) => this.handleExportFolderClick(e, folder, 'md')
click: e => this.handleExportFolderClick(e, folder, 'md')
}
]
},
@@ -172,12 +171,12 @@ class StorageItem extends React.Component {
},
{
label: i18n.__('Delete Folder'),
click: (e) => this.handleFolderDeleteClick(e, folder)
click: e => this.handleFolderDeleteClick(e, folder)
}
])
}
handleRenameFolderClick (e, folder) {
handleRenameFolderClick(e, folder) {
const { storage } = this.props
modal.open(RenameFolderModal, {
storage,
@@ -185,20 +184,19 @@ class StorageItem extends React.Component {
})
}
handleExportFolderClick (e, folder, fileType) {
handleExportFolderClick(e, folder, fileType) {
const options = {
properties: ['openDirectory', 'createDirectory'],
buttonLabel: i18n.__('Select directory'),
title: i18n.__('Select a folder to export the files to'),
multiSelections: false
}
dialog.showOpenDialog(remote.getCurrentWindow(), options,
(paths) => {
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) {
const { storage, dispatch } = this.props
dataApi
.exportFolder(storage.key, folder.key, fileType, paths[0])
.then((data) => {
.then(data => {
dispatch({
type: 'EXPORT_FOLDER',
storage: data.storage,
@@ -224,66 +222,74 @@ class StorageItem extends React.Component {
})
}
handleFolderDeleteClick (e, folder) {
handleFolderDeleteClick(e, folder) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
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')]
})
if (index === 0) {
const { storage, dispatch } = this.props
dataApi
.deleteFolder(storage.key, folder.key)
.then((data) => {
dispatch({
type: 'DELETE_FOLDER',
storage: data.storage,
folderKey: data.folderKey
})
dataApi.deleteFolder(storage.key, folder.key).then(data => {
dispatch({
type: 'DELETE_FOLDER',
storage: data.storage,
folderKey: data.folderKey
})
})
}
}
handleDragEnter (e, key) {
handleDragEnter(e, key) {
e.preventDefault()
if (this.state.draggedOver === key) { return }
if (this.state.draggedOver === key) {
return
}
this.setState({
draggedOver: key
})
}
handleDragLeave (e) {
handleDragLeave(e) {
e.preventDefault()
if (this.state.draggedOver === null) { return }
if (this.state.draggedOver === null) {
return
}
this.setState({
draggedOver: null
})
}
dropNote (storage, folder, dispatch, location, noteData) {
noteData = noteData.filter((note) => folder.key !== note.folder)
dropNote(storage, folder, dispatch, location, noteData) {
noteData = noteData.filter(note => folder.key !== note.folder)
if (noteData.length === 0) return
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) => {
dispatch({
type: 'MOVE_NOTE',
originNote: noteData.find((note) => note.content === newNote.oldContent),
note: newNote
.then(createdNoteData => {
createdNoteData.forEach(newNote => {
dispatch({
type: 'MOVE_NOTE',
originNote: noteData.find(
note => note.content === newNote.oldContent
),
note: newNote
})
})
})
})
.catch((err) => {
console.error(`error on delete notes: ${err}`)
})
.catch(err => {
console.error(`error on delete notes: ${err}`)
})
}
handleDrop (e, storage, folder, dispatch, location) {
handleDrop(e, storage, folder, dispatch, location) {
e.preventDefault()
if (this.state.draggedOver !== null) {
this.setState({
@@ -294,21 +300,37 @@ class StorageItem extends React.Component {
this.dropNote(storage, folder, dispatch, location, noteData)
}
render () {
render() {
const { storage, location, isFolded, data, dispatch } = this.props
const { folderNoteMap, trashedSet } = data
const SortableStorageItemChild = SortableElement(StorageItemChild)
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 isActive = !!(location.pathname.match(folderRegex))
const folderRegex = new RegExp(
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)
let noteCount = 0
if (noteSet) {
let trashedNoteCount = 0
const noteKeys = noteSet.map(noteKey => { return noteKey })
const noteKeys = noteSet.map(noteKey => {
return noteKey
})
trashedSet.toJS().forEach(trashedKey => {
if (noteKeys.some(noteKey => { return noteKey === trashedKey })) trashedNoteCount++
if (
noteKeys.some(noteKey => {
return noteKey === trashedKey
})
)
trashedNoteCount++
})
noteCount = noteSet.size - trashedNoteCount
}
@@ -317,73 +339,80 @@ class StorageItem extends React.Component {
key={folder.key}
index={index}
isActive={isActive || folder.key === this.state.draggedOver}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
handleButtonClick={e => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={e => this.handleFolderButtonContextMenu(e, folder)}
folderName={folder.name}
folderColor={folder.color}
isFolded={isFolded}
noteCount={noteCount}
handleDrop={(e) => {
handleDrop={e => {
this.handleDrop(e, storage, folder, dispatch, location)
}}
handleDragEnter={(e) => {
handleDragEnter={e => {
this.handleDragEnter(e, folder.key)
}}
handleDragLeave={(e) => {
handleDragLeave={e => {
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 (
<div styleName={isFolded ? 'root--folded' : 'root'}
key={storage.key}
>
<div styleName={isActive
? 'header--active'
: 'header'
}
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
<div styleName={isFolded ? 'root--folded' : 'root'} key={storage.key}>
<div
styleName={isActive ? 'header--active' : 'header'}
onContextMenu={e => this.handleHeaderContextMenu(e)}
>
<button styleName='header-toggleButton'
onMouseDown={(e) => this.handleToggleButtonClick(e)}
<button
styleName='header-toggleButton'
onMouseDown={e => this.handleToggleButtonClick(e)}
>
<img src={this.state.isOpen
? '../resources/icon/icon-down.svg'
: '../resources/icon/icon-right.svg'
}
<img
src={
this.state.isOpen
? '../resources/icon/icon-down.svg'
: '../resources/icon/icon-right.svg'
}
/>
</button>
{!isFolded &&
<button styleName='header-addFolderButton'
onClick={(e) => this.handleAddFolderButtonClick(e)}
{!isFolded && (
<button
styleName='header-addFolderButton'
onClick={e => this.handleAddFolderButtonClick(e)}
>
<img src='../resources/icon/icon-plus.svg' />
</button>
}
)}
<button styleName='header-info'
onClick={(e) => this.handleHeaderInfoClick(e)}
<button
styleName='header-info'
onClick={e => this.handleHeaderInfoClick(e)}
>
<span>
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
{isFolded
? _.truncate(storage.name, { length: 1, omission: '' })
: storage.name}
</span>
{isFolded &&
{isFolded && (
<span styleName='header-info--folded-tooltip'>
{storage.name}
</span>
}
)}
</button>
</div>
{this.state.isOpen &&
<div>
{folderList}
</div>
}
{this.state.isOpen && <div>{folderList}</div>}
</div>
)
}

View File

@@ -4,14 +4,17 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const TagButton = ({
onClick, isTagActive
}) => (
<button styleName={isTagActive ? 'active-button' : 'non-active-button'} onClick={onClick}>
<img src={isTagActive
? '../resources/icon/icon-tag-active.svg'
: '../resources/icon/icon-tag.svg'
}
const TagButton = ({ onClick, isTagActive }) => (
<button
styleName={isTagActive ? 'active-button' : 'non-active-button'}
onClick={onClick}
>
<img
src={
isTagActive
? '../resources/icon/icon-tag-active.svg'
: '../resources/icon/icon-tag.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Tags')}</span>
</button>

View File

@@ -16,7 +16,7 @@ import EventEmitter from 'browser/main/lib/eventEmitter'
import PreferenceButton from './PreferenceButton'
import ListButton from './ListButton'
import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc'
import { SortableContainer } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import { remote } from 'electron'
@@ -24,13 +24,13 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import ColorPicker from 'browser/components/ColorPicker'
import { every, sortBy } from 'lodash'
function matchActiveTags (tags, activeTags) {
function matchActiveTags(tags, activeTags) {
return every(activeTags, v => tags.indexOf(v) >= 0)
}
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -47,24 +47,32 @@ class SideNav extends React.Component {
this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
}
componentDidMount () {
componentDidMount() {
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
}
componentWillUnmount () {
componentWillUnmount() {
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
}
deleteTag (tag) {
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Confirm tag deletion'),
detail: i18n.__('This will permanently remove this tag.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
deleteTag(tag) {
const selectedButton = remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
type: 'warning',
message: i18n.__('Confirm tag deletion'),
detail: i18n.__('This will permanently remove this tag.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}
)
if (selectedButton === 0) {
const { data, dispatch, location, match: { params } } = this.props
const {
data,
dispatch,
location,
match: { params }
} = this.props
const notes = data.noteMap
.map(note => note)
@@ -78,44 +86,48 @@ class SideNav extends React.Component {
return note
})
Promise
.all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
.then(updatedNotes => {
updatedNotes.forEach(note => {
dispatch({
type: 'UPDATE_NOTE',
note
})
Promise.all(
notes.map(note => dataApi.updateNote(note.storage, note.key, note))
).then(updatedNotes => {
updatedNotes.forEach(note => {
dispatch({
type: 'UPDATE_NOTE',
note
})
if (location.pathname.match('/tags')) {
const tags = params.tagname.split(' ')
const index = tags.indexOf(tag)
if (index !== -1) {
tags.splice(index, 1)
dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`))
}
}
})
if (location.pathname.match('/tags')) {
const tags = params.tagname.split(' ')
const index = tags.indexOf(tag)
if (index !== -1) {
tags.splice(index, 1)
dispatch(
push(
`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`
)
)
}
}
})
}
}
handleMenuButtonClick (e) {
handleMenuButtonClick(e) {
openModal(PreferencesModal)
}
handleHomeButtonClick (e) {
handleHomeButtonClick(e) {
const { dispatch } = this.props
dispatch(push('/home'))
}
handleStarredButtonClick (e) {
handleStarredButtonClick(e) {
const { dispatch } = this.props
dispatch(push('/starred'))
}
handleTagContextMenu (e, tag) {
handleTagContextMenu(e, tag) {
const menu = []
menu.push({
@@ -125,13 +137,17 @@ class SideNav extends React.Component {
menu.push({
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)
}
dismissColorPicker () {
dismissColorPicker() {
this.setState({
colorPicker: {
show: false
@@ -139,7 +155,7 @@ class SideNav extends React.Component {
})
}
displayColorPicker (tagName, rect) {
displayColorPicker(tagName, rect) {
const { config } = this.props
this.setState({
colorPicker: {
@@ -151,10 +167,17 @@ class SideNav extends React.Component {
})
}
handleColorPickerConfirm (color) {
const { dispatch, config: {coloredTags} } = this.props
const { colorPicker: { tagName } } = this.state
const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex})
handleColorPickerConfirm(color) {
const {
dispatch,
config: { coloredTags }
} = this.props
const {
colorPicker: { tagName }
} = this.state
const newColoredTags = Object.assign({}, coloredTags, {
[tagName]: color.hex
})
const config = { coloredTags: newColoredTags }
ConfigManager.set(config)
@@ -165,9 +188,14 @@ class SideNav extends React.Component {
this.dismissColorPicker()
}
handleColorPickerReset () {
const { dispatch, config: {coloredTags} } = this.props
const { colorPicker: { tagName } } = this.state
handleColorPickerReset() {
const {
dispatch,
config: { coloredTags }
} = this.props
const {
colorPicker: { tagName }
} = this.state
const newColoredTags = Object.assign({}, coloredTags)
delete newColoredTags[tagName]
@@ -181,43 +209,41 @@ class SideNav extends React.Component {
this.dismissColorPicker()
}
handleToggleButtonClick (e) {
handleToggleButtonClick(e) {
const { dispatch, config } = this.props
ConfigManager.set({isSideNavFolded: !config.isSideNavFolded})
ConfigManager.set({ isSideNavFolded: !config.isSideNavFolded })
dispatch({
type: 'SET_IS_SIDENAV_FOLDED',
isFolded: !config.isSideNavFolded
})
}
handleTrashedButtonClick (e) {
handleTrashedButtonClick(e) {
const { dispatch } = this.props
dispatch(push('/trashed'))
}
handleSwitchFoldersButtonClick () {
handleSwitchFoldersButtonClick() {
const { dispatch } = this.props
dispatch(push('/home'))
}
handleSwitchTagsButtonClick () {
handleSwitchTagsButtonClick() {
const { dispatch } = this.props
dispatch(push('/alltags'))
}
onSortEnd (storage) {
return ({oldIndex, newIndex}) => {
onSortEnd(storage) {
return ({ oldIndex, newIndex }) => {
const { dispatch } = this.props
dataApi
.reorderFolder(storage.key, oldIndex, newIndex)
.then((data) => {
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
})
dataApi.reorderFolder(storage.key, oldIndex, newIndex).then(data => {
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
})
}
}
SideNavComponent (isFolded, storageList) {
SideNavComponent(isFolded, storageList) {
const { location, data, config } = this.props
const isHomeActive = !!location.pathname.match(/^\/home$/)
@@ -227,25 +253,35 @@ class SideNav extends React.Component {
let component
// TagsMode is not selected
if (!location.pathname.match('/tags') && !location.pathname.match('/alltags')) {
if (
!location.pathname.match('/tags') &&
!location.pathname.match('/alltags')
) {
component = (
<div>
<SideNavFilter
isFolded={isFolded}
isHomeActive={isHomeActive}
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
handleAllNotesButtonClick={e => this.handleHomeButtonClick(e)}
isStarredActive={isStarredActive}
isTrashedActive={isTrashedActive}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
handleStarredButtonClick={e => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={e => this.handleTrashedButtonClick(e)}
counterTotalNote={
data.noteMap._map.size - data.trashedSet._set.size
}
counterStarredNote={data.starredSet._set.size}
counterDelNote={data.trashedSet._set.size}
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(
this
)}
/>
<StorageList storageList={storageList} isFolded={isFolded} />
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
<NavToggleButton
isFolded={isFolded}
handleToggleButtonClick={this.handleToggleButtonClick.bind(this)}
/>
</div>
)
} else {
@@ -257,22 +293,26 @@ class SideNav extends React.Component {
</div>
<div styleName='tag-control-sortTagsBy'>
<i className='fa fa-angle-down' />
<select styleName='tag-control-sortTagsBy-select'
<select
styleName='tag-control-sortTagsBy-select'
title={i18n.__('Select filter mode')}
value={config.sortTagsBy}
onChange={(e) => this.handleSortTagsByChange(e)}
onChange={e => this.handleSortTagsByChange(e)}
>
<option title='Sort alphabetically'
value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
<option title='Sort by update time'
value='COUNTER'>{i18n.__('Counter')}</option>
<option title='Sort alphabetically' value='ALPHABETICAL'>
{i18n.__('Alphabetically')}
</option>
<option title='Sort by update time' value='COUNTER'>
{i18n.__('Counter')}
</option>
</select>
</div>
</div>
<div styleName='tagList'>
{this.tagListComponent(data)}
</div>
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
<div styleName='tagList'>{this.tagListComponent(data)}</div>
<NavToggleButton
isFolded={isFolded}
handleToggleButtonClick={this.handleToggleButtonClick.bind(this)}
/>
</div>
)
}
@@ -280,82 +320,84 @@ class SideNav extends React.Component {
return component
}
tagListComponent () {
tagListComponent() {
const { data, location, config } = this.props
const { colorPicker } = this.state
const activeTags = this.getActiveTags(location.pathname)
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
let tagList = sortBy(data.tagNoteMap.map(
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
).filter(
tag => tag.size > 0
), ['name'])
let tagList = sortBy(
data.tagNoteMap
.map((tag, name) => ({
name,
size: tag.size,
related: relatedTags.has(name)
}))
.filter(tag => tag.size > 0),
['name']
)
if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
const notesTags = data.noteMap.map(note => note.tags)
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
})
}
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)) {
tagList = tagList.filter(
tag => tag.related
if (config.ui.showOnlyRelatedTags && relatedTags.size > 0) {
tagList = tagList.filter(tag => tag.related)
}
return tagList.map(tag => {
return (
<TagListItem
name={tag.name}
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
handleContextMenu={this.handleTagContextMenu.bind(this)}
isActive={
this.getTagActive(location.pathname, tag.name) ||
colorPicker.tagName === tag.name
}
isRelated={tag.related}
key={tag.name}
count={tag.size}
color={config.coloredTags[tag.name]}
/>
)
}
return (
tagList.map(tag => {
return (
<TagListItem
name={tag.name}
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
handleContextMenu={this.handleTagContextMenu.bind(this)}
isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)}
isRelated={tag.related}
key={tag.name}
count={tag.size}
color={config.coloredTags[tag.name]}
/>
)
})
)
})
}
getRelatedTags (activeTags, noteMap) {
getRelatedTags(activeTags, noteMap) {
if (activeTags.length === 0) {
return new Set()
}
const relatedNotes = noteMap.map(
note => ({key: note.key, tags: note.tags})
).filter(
note => activeTags.every(tag => note.tags.includes(tag))
)
const relatedNotes = noteMap
.map(note => ({ key: note.key, tags: note.tags }))
.filter(note => activeTags.every(tag => note.tags.includes(tag)))
const relatedTags = new Set()
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
return relatedTags
}
getTagActive (path, tag) {
getTagActive(path, tag) {
return this.getActiveTags(path).includes(tag)
}
getActiveTags (path) {
getActiveTags(path) {
const pathSegments = path.split('/')
const tags = pathSegments[pathSegments.length - 1]
return (tags === 'alltags')
? []
: decodeURIComponent(tags).split(' ')
return tags === 'alltags' ? [] : decodeURIComponent(tags).split(' ')
}
handleClickTagListItem (name) {
handleClickTagListItem(name) {
const { dispatch } = this.props
dispatch(push(`/tags/${encodeURIComponent(name)}`))
}
handleSortTagsByChange (e) {
handleSortTagsByChange(e) {
const { dispatch } = this.props
const config = {
@@ -369,7 +411,7 @@ class SideNav extends React.Component {
})
}
handleClickNarrowToTag (tag) {
handleClickNarrowToTag(tag) {
const { dispatch, location } = this.props
const listOfTags = this.getActiveTags(location.pathname)
const indexOfTag = listOfTags.indexOf(tag)
@@ -381,33 +423,38 @@ class SideNav extends React.Component {
dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`))
}
emptyTrash (entries) {
emptyTrash(entries) {
const { dispatch } = this.props
const deletionPromises = entries.map((note) => {
const deletionPromises = entries.map(note => {
return dataApi.deleteNote(note.storage, note.key)
})
const { confirmDeletion } = this.props.config.ui
if (!confirmDeleteNote(confirmDeletion, true)) return
Promise.all(deletionPromises)
.then((arrayOfStorageAndNoteKeys) => {
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
.then(arrayOfStorageAndNoteKeys => {
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
})
})
.catch(err => {
console.error('Cannot Delete note: ' + err)
})
})
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
}
handleFilterButtonContextMenu (event) {
handleFilterButtonContextMenu(event) {
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([
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
{
label: i18n.__('Empty Trash'),
click: () => this.emptyTrash(trashedNotes)
}
])
}
render () {
render() {
const { data, location, config, dispatch } = this.props
const { colorPicker: colorPickerState } = this.state
@@ -415,16 +462,18 @@ class SideNav extends React.Component {
const storageList = data.storageMap.map((storage, key) => {
const SortableStorageItem = SortableContainer(StorageItem)
return <SortableStorageItem
key={storage.key}
storage={storage}
data={data}
location={location}
isFolded={isFolded}
dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
/>
return (
<SortableStorageItem
key={storage.key}
storage={storage}
data={data}
location={location}
isFolded={isFolded}
dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
/>
)
})
let colorPicker
@@ -444,15 +493,22 @@ class SideNav extends React.Component {
if (!isFolded) style.width = this.props.width
const isTagActive = /tag/.test(location.pathname)
return (
<div className='SideNav'
<div
className='SideNav'
styleName={isFolded ? 'root--folded' : 'root'}
tabIndex='1'
style={style}
>
<div styleName='top'>
<div styleName='switch-buttons'>
<ListButton onClick={this.handleSwitchFoldersButtonClick.bind(this)} isTagActive={isTagActive} />
<TagButton onClick={this.handleSwitchTagsButtonClick.bind(this)} isTagActive={isTagActive} />
<ListButton
onClick={this.handleSwitchFoldersButtonClick.bind(this)}
isTagActive={isTagActive}
/>
<TagButton
onClick={this.handleSwitchTagsButtonClick.bind(this)}
isTagActive={isTagActive}
/>
</div>
<div>
<PreferenceButton onClick={this.handleMenuButtonClick} />

View File

@@ -11,30 +11,43 @@ const electron = require('electron')
const { remote, ipcRenderer } = electron
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 {
constructor (props) {
constructor(props) {
super(props)
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this)
this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this)
}
componentDidMount () {
componentDidMount() {
EventEmitter.on('status:zoomin', this.handleZoomInMenuItem)
EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem)
EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem)
}
componentWillUnmount () {
componentWillUnmount() {
EventEmitter.off('status:zoomin', this.handleZoomInMenuItem)
EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem)
EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem)
}
updateApp () {
updateApp() {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Update Boostnote'),
@@ -47,10 +60,10 @@ class StatusBar extends React.Component {
}
}
handleZoomButtonClick (e) {
handleZoomButtonClick(e) {
const templates = []
zoomOptions.forEach((zoom) => {
zoomOptions.forEach(zoom => {
templates.push({
label: Math.floor(zoom * 100) + '%',
click: () => this.handleZoomMenuItemClick(zoom)
@@ -60,7 +73,7 @@ class StatusBar extends React.Component {
context.popup(templates)
}
handleZoomMenuItemClick (zoomFactor) {
handleZoomMenuItemClick(zoomFactor) {
const { dispatch } = this.props
ZoomManager.setZoom(zoomFactor)
dispatch({
@@ -69,40 +82,36 @@ class StatusBar extends React.Component {
})
}
handleZoomInMenuItem () {
handleZoomInMenuItem() {
const zoomFactor = ZoomManager.getZoom() + 0.1
this.handleZoomMenuItemClick(zoomFactor)
}
handleZoomOutMenuItem () {
handleZoomOutMenuItem() {
const zoomFactor = ZoomManager.getZoom() - 0.1
this.handleZoomMenuItemClick(zoomFactor)
}
handleZoomResetMenuItem () {
handleZoomResetMenuItem() {
this.handleZoomMenuItemClick(1.0)
}
render () {
render() {
const { config, status } = this.context
return (
<div className='StatusBar'
styleName='root'
>
<button styleName='zoom'
onClick={(e) => this.handleZoomButtonClick(e)}
>
<div className='StatusBar' styleName='root'>
<button styleName='zoom' onClick={e => this.handleZoomButtonClick(e)}>
<img src='../resources/icon/icon-zoom.svg' />
<span>{Math.floor(config.zoom * 100)}%</span>
</button>
{status.updateReady
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' /> {i18n.__('Ready to Update!')}
{status.updateReady ? (
<button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' />{' '}
{i18n.__('Ready to Update!')}
</button>
: null
}
) : null}
</div>
)
}

View File

@@ -11,7 +11,7 @@ import CInput from 'react-composition-input'
import { push } from 'connected-react-router'
class TopBar extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -33,19 +33,25 @@ class TopBar extends React.Component {
this.handleSearchChange = this.handleSearchChange.bind(this)
this.handleSearchClearButton = this.handleSearchClearButton.bind(this)
this.debouncedUpdateKeyword = debounce((keyword) => {
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
this.setState({
search: keyword
})
ee.emit('top:search', keyword)
}, 1000 / 60, {
maxWait: 1000 / 8
})
this.debouncedUpdateKeyword = debounce(
keyword => {
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
this.setState({
search: keyword
})
ee.emit('top:search', keyword)
},
1000 / 60,
{
maxWait: 1000 / 8
}
)
}
componentDidMount () {
const { match: { params } } = this.props
componentDidMount() {
const {
match: { params }
} = this.props
const searchWord = params && params.searchword
if (searchWord !== undefined) {
this.setState({
@@ -57,12 +63,12 @@ class TopBar extends React.Component {
ee.on('code:init', this.codeInitHandler)
}
componentWillUnmount () {
componentWillUnmount() {
ee.off('top:focus-search', this.focusSearchHandler)
ee.off('code:init', this.codeInitHandler)
}
handleSearchClearButton (e) {
handleSearchClearButton(e) {
const { dispatch } = this.props
this.setState({
search: '',
@@ -74,7 +80,7 @@ class TopBar extends React.Component {
this.debouncedUpdateKeyword('')
}
handleKeyDown (e) {
handleKeyDown(e) {
// Re-apply search field on ENTER key
if (e.keyCode === 13) {
this.debouncedUpdateKeyword(e.target.value)
@@ -98,18 +104,18 @@ class TopBar extends React.Component {
}
}
handleSearchChange (e) {
handleSearchChange(e) {
const keyword = e.target.value
this.debouncedUpdateKeyword(keyword)
}
handleSearchFocus (e) {
handleSearchFocus(e) {
this.setState({
isSearching: true
})
}
handleSearchBlur (e) {
handleSearchBlur(e) {
e.stopPropagation()
let el = e.relatedTarget
@@ -128,7 +134,7 @@ class TopBar extends React.Component {
}
}
handleOnSearchFocus () {
handleOnSearchFocus() {
const el = this.refs.search.childNodes[0]
if (this.state.isSearching) {
el.blur()
@@ -137,20 +143,22 @@ class TopBar extends React.Component {
}
}
handleCodeInit () {
handleCodeInit() {
ee.emit('top:search', this.refs.searchInput.value || '')
}
render () {
render() {
const { config, style, location } = this.props
return (
<div className='TopBar'
<div
className='TopBar'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
style={style}
>
<div styleName='control'>
<div styleName='control-search'>
<div styleName='control-search-input'
<div
styleName='control-search-input'
onFocus={this.handleSearchFocus}
onBlur={this.handleSearchBlur}
tabIndex='-1'
@@ -165,27 +173,33 @@ class TopBar extends React.Component {
type='text'
className='searchInput'
/>
{this.state.search !== '' &&
<button styleName='control-search-input-clear'
{this.state.search !== '' && (
<button
styleName='control-search-input-clear'
onClick={this.handleSearchClearButton}
>
<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>
}
)}
</div>
</div>
</div>
{location.pathname === '/trashed' ? ''
: <NewNoteButton
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'location',
'match'
])}
/>}
{location.pathname === '/trashed' ? (
''
) : (
<NewNoteButton
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'location',
'match'
])}
/>
)}
</div>
)
}

View File

@@ -17,11 +17,11 @@ const electron = require('electron')
const { remote, ipcRenderer } = electron
const { dialog } = remote
document.addEventListener('drop', function (e) {
document.addEventListener('drop', function(e) {
e.preventDefault()
e.stopPropagation()
})
document.addEventListener('dragover', function (e) {
document.addEventListener('dragover', function(e) {
e.preventDefault()
e.stopPropagation()
})
@@ -33,7 +33,7 @@ let isAltWithMouse = false
let isAltWithOtherKey = false
let isOtherKey = false
document.addEventListener('keydown', function (e) {
document.addEventListener('keydown', function(e) {
if (e.key === 'Alt') {
isAltPressing = true
if (isOtherKey) {
@@ -47,13 +47,13 @@ document.addEventListener('keydown', function (e) {
}
})
document.addEventListener('mousedown', function (e) {
document.addEventListener('mousedown', function(e) {
if (isAltPressing) {
isAltWithMouse = true
}
})
document.addEventListener('keyup', function (e) {
document.addEventListener('keyup', function(e) {
if (e.key === 'Alt') {
if (isAltWithMouse || isAltWithOtherKey) {
e.preventDefault()
@@ -65,14 +65,13 @@ document.addEventListener('keyup', function (e) {
}
})
document.addEventListener('click', function (e) {
document.addEventListener('click', function(e) {
const className = e.target.className
if (!className && typeof (className) !== 'string') return
if (!className && typeof className !== 'string') return
const isInfoButton = className.includes('infoButton')
const offsetParent = e.target.offsetParent
const isInfoPanel = offsetParent !== null
? offsetParent.className.includes('infoPanel')
: false
const isInfoPanel =
offsetParent !== null ? offsetParent.className.includes('infoPanel') : false
if (isInfoButton || isInfoPanel) return
const infoPanel = document.querySelector('.infoPanel')
if (infoPanel) infoPanel.style.display = 'none'
@@ -80,11 +79,11 @@ document.addEventListener('click', function (e) {
const el = document.getElementById('content')
function notify (...args) {
function notify(...args) {
return new window.Notification(...args)
}
function updateApp () {
function updateApp() {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Update Boostnote'),
@@ -97,7 +96,7 @@ function updateApp () {
}
}
ReactDOM.render((
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<Fragment>
@@ -112,36 +111,41 @@ ReactDOM.render((
{/* storages */}
<Redirect path='/storages' to='/home' 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>
<DevTools />
</Fragment>
</ConnectedRouter>
</Provider>
), el, function () {
const loadingCover = document.getElementById('loadingCover')
loadingCover.parentNode.removeChild(loadingCover)
</Provider>,
el,
function() {
const loadingCover = document.getElementById('loadingCover')
loadingCover.parentNode.removeChild(loadingCover)
ipcRenderer.on('update-ready', function () {
store.dispatch({
type: 'UPDATE_AVAILABLE'
ipcRenderer.on('update-ready', function() {
store.dispatch({
type: 'UPDATE_AVAILABLE'
})
notify('Update ready!', {
body: 'New Boostnote is ready to be installed.'
})
updateApp()
})
notify('Update ready!', {
body: 'New Boostnote is ready to be installed.'
})
updateApp()
})
ipcRenderer.on('update-found', function () {
notify('Update found!', {
body: 'Preparing to update...'
ipcRenderer.on('update-found', function() {
notify('Update found!', {
body: 'Preparing to update...'
})
})
})
ipcRenderer.send('update-check', 'check-update')
window.addEventListener('online', function () {
if (!store.getState().status.updateReady) {
ipcRenderer.send('update-check', 'check-update')
}
})
})
ipcRenderer.send('update-check', 'check-update')
window.addEventListener('online', function() {
if (!store.getState().status.updateReady) {
ipcRenderer.send('update-check', 'check-update')
}
})
}
)

View File

@@ -22,7 +22,7 @@ if (!getSendEventCond()) {
})
}
function convertPlatformName (platformName) {
function convertPlatformName(platformName) {
if (platformName === 'darwin') {
return 'MacOS'
} else if (platformName === 'win32') {
@@ -34,16 +34,16 @@ function convertPlatformName (platformName) {
}
}
function getSendEventCond () {
function getSendEventCond() {
const isDev = process.env.NODE_ENV !== 'production'
const isDisable = !ConfigManager.default.get().amaEnabled
const isOffline = !window.navigator.onLine
return isDev || isDisable || isOffline
}
function initAwsMobileAnalytics () {
function initAwsMobileAnalytics() {
if (getSendEventCond()) return
AWS.config.credentials.get((err) => {
AWS.config.credentials.get(err => {
if (!err) {
recordDynamicCustomEvent('APP_STARTED')
recordStaticCustomEvent()
@@ -51,7 +51,7 @@ function initAwsMobileAnalytics () {
})
}
function recordDynamicCustomEvent (type, options = {}) {
function recordDynamicCustomEvent(type, options = {}) {
if (getSendEventCond()) return
try {
mobileAnalyticsClient.recordEvent(type, options)
@@ -62,7 +62,7 @@ function recordDynamicCustomEvent (type, options = {}) {
}
}
function recordStaticCustomEvent () {
function recordStaticCustomEvent() {
if (getSendEventCond()) return
try {
mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', {

View File

@@ -1,25 +1,24 @@
let callees = []
function bind (name, el) {
function bind(name, el) {
callees.push({
name: name,
element: el
})
}
function release (el) {
callees = callees.filter((callee) => callee.element !== el)
function release(el) {
callees = callees.filter(callee => callee.element !== el)
}
function fire (command) {
function fire(command) {
console.info('COMMAND >>', command)
const splitted = command.split(':')
const target = splitted[0]
const targetCommand = splitted[1]
const targetCallees = callees
.filter((callee) => callee.name === target)
const targetCallees = callees.filter(callee => callee.name === target)
targetCallees.forEach((callee) => {
targetCallees.forEach(callee => {
callee.element.fire(targetCommand)
})
}

View File

@@ -33,7 +33,9 @@ export const DEFAULT_CONFIG = {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
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',
prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F',
sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S',
@@ -116,7 +118,7 @@ export const DEFAULT_CONFIG = {
coloredTags: {}
}
function validate (config) {
function validate(config) {
if (!_.isObject(config)) return false
if (!_.isNumber(config.zoom) || config.zoom < 0) return false
if (!_.isBoolean(config.isSideNavFolded)) return false
@@ -125,13 +127,17 @@ function validate (config) {
return true
}
function _save (config) {
function _save(config) {
window.localStorage.setItem('config', JSON.stringify(config))
}
function get () {
function get() {
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
try {
@@ -145,7 +151,10 @@ function get () {
_save(config)
}
config.autoUpdateEnabled = electronConfig.get('autoUpdateEnabled', config.autoUpdateEnabled)
config.autoUpdateEnabled = electronConfig.get(
'autoUpdateEnabled',
config.autoUpdateEnabled
)
if (!isInitialized) {
isInitialized = true
@@ -157,7 +166,9 @@ function get () {
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) {
editorTheme.setAttribute('href', theme.path)
@@ -169,7 +180,7 @@ function get () {
return config
}
function set (updates) {
function set(updates) {
const currentConfig = get()
const arrangedUpdates = updates
@@ -177,7 +188,12 @@ function set (updates) {
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')
_save(newConfig)
@@ -197,7 +213,9 @@ function set (updates) {
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) {
editorTheme.setAttribute('href', newTheme.path)
@@ -211,20 +229,45 @@ function set (updates) {
ee.emit('config-renew')
}
function assignConfigValues (originalConfig, rcConfig) {
function assignConfigValues(originalConfig, rcConfig) {
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
config.blog = Object.assign({}, DEFAULT_CONFIG.blog, originalConfig.blog, rcConfig.blog)
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
config.hotkey = Object.assign(
{},
DEFAULT_CONFIG.hotkey,
originalConfig.hotkey,
rcConfig.hotkey
)
config.blog = Object.assign(
{},
DEFAULT_CONFIG.blog,
originalConfig.blog,
rcConfig.blog
)
config.ui = Object.assign(
{},
DEFAULT_CONFIG.ui,
originalConfig.ui,
rcConfig.ui
)
config.editor = Object.assign(
{},
DEFAULT_CONFIG.editor,
originalConfig.editor,
rcConfig.editor
)
config.preview = Object.assign(
{},
DEFAULT_CONFIG.preview,
originalConfig.preview,
rcConfig.preview
)
rewriteHotkey(config)
return config
}
function rewriteHotkey (config) {
function rewriteHotkey(config) {
const keys = [...Object.keys(config.hotkey)]
keys.forEach(key => {
config.hotkey[key] = config.hotkey[key].replace(/Cmd\s/g, 'Command ')

View File

@@ -5,20 +5,20 @@ const { remote } = electron
_init()
function _init () {
function _init() {
setZoom(getZoom(), true)
}
function _saveZoom (zoomFactor) {
ConfigManager.set({zoom: zoomFactor})
function _saveZoom(zoomFactor) {
ConfigManager.set({ zoom: zoomFactor })
}
function setZoom (zoomFactor, noSave = false) {
function setZoom(zoomFactor, noSave = false) {
if (!noSave) _saveZoom(zoomFactor)
remote.getCurrentWebContents().setZoomFactor(zoomFactor)
}
function getZoom () {
function getZoom() {
const config = ConfigManager.get()
return config.zoom

View File

@@ -16,7 +16,7 @@ const CSON = require('@rokt33r/season')
* 3. fetch notes & folders
* 4. return `{storage: {...} folders: [folder]}`
*/
function addStorage (input) {
function addStorage(input) {
if (!_.isString(input.path)) {
return Promise.reject(new Error('Path must be a string.'))
}
@@ -29,7 +29,7 @@ function addStorage (input) {
rawStorages = []
}
let key = keygen()
while (rawStorages.some((storage) => storage.key === key)) {
while (rawStorages.some(storage => storage.key === key)) {
key = keygen()
}
@@ -43,7 +43,7 @@ function addStorage (input) {
return Promise.resolve(newStorage)
.then(resolveStorageData)
.then(function saveMetadataToLocalStorage (resolvedStorage) {
.then(function saveMetadataToLocalStorage(resolvedStorage) {
newStorage = resolvedStorage
rawStorages.push({
key: newStorage.key,
@@ -56,27 +56,29 @@ function addStorage (input) {
localStorage.setItem('storages', JSON.stringify(rawStorages))
return newStorage
})
.then(function (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
let unknownCount = 0
notes.forEach((note) => {
if (!storage.folders.some((folder) => note.folder === folder.key)) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
})
if (unknownCount > 0) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
.then(function(storage) {
return resolveStorageNotes(storage).then(notes => {
let unknownCount = 0
notes.forEach(note => {
if (!storage.folders.some(folder => note.folder === folder.key)) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
return notes
})
if (unknownCount > 0) {
CSON.writeFileSync(
path.join(storage.path, 'boostnote.json'),
_.pick(storage, ['folders', 'version'])
)
}
return notes
})
})
.then(function returnValue (notes) {
.then(function returnValue(notes) {
return {
storage: newStorage,
notes

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ const path = require('path')
* @param {String} dstPath
* @return {Promise} an image path
*/
function copyFile (srcPath, dstPath) {
function copyFile(srcPath, dstPath) {
if (!path.extname(dstPath)) {
dstPath = path.join(dstPath, path.basename(srcPath))
}

View File

@@ -22,7 +22,7 @@ const { findStorage } = require('browser/lib/findStorage')
* }
* ```
*/
function createFolder (storageKey, input) {
function createFolder(storageKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
@@ -34,26 +34,28 @@ function createFolder (storageKey, input) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function createFolder (storage) {
let key = keygen()
while (storage.folders.some((folder) => folder.key === key)) {
key = keygen()
}
const newFolder = {
key,
color: input.color,
name: input.name
}
return resolveStorageData(targetStorage).then(function createFolder(storage) {
let key = keygen()
while (storage.folders.some(folder => folder.key === key)) {
key = keygen()
}
const newFolder = {
key,
color: input.color,
name: input.name
}
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 {
storage
}
})
return {
storage
}
})
}
module.exports = createFolder

View File

@@ -6,9 +6,11 @@ const path = require('path')
const CSON = require('@rokt33r/season')
const { findStorage } = require('browser/lib/findStorage')
function validateInput (input) {
function validateInput(input) {
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 = ''
input.isStarred = !!input.isStarred
input.isTrashed = !!input.isTrashed
@@ -21,20 +23,24 @@ function validateInput (input) {
case 'SNIPPET_NOTE':
if (!_.isString(input.description)) input.description = ''
if (!_.isArray(input.snippets)) {
input.snippets = [{
name: '',
mode: 'text',
content: '',
linesHighlighted: []
}]
input.snippets = [
{
name: '',
mode: 'text',
content: '',
linesHighlighted: []
}
]
}
break
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.'
)
}
}
function createNote (storageKey, input) {
function createNote(storageKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
@@ -47,13 +53,13 @@ function createNote (storageKey, input) {
}
return resolveStorageData(targetStorage)
.then(function checkFolderExists (storage) {
if (_.find(storage.folders, {key: input.folder}) == null) {
throw new Error('Target folder doesn\'t exist.')
.then(function checkFolderExists(storage) {
if (_.find(storage.folders, { key: input.folder }) == null) {
throw new Error("Target folder doesn't exist.")
}
return storage
})
.then(function saveNote (storage) {
.then(function saveNote(storage) {
let key = keygen(true)
let isUnique = false
while (!isUnique) {
@@ -68,7 +74,8 @@ function createNote (storageKey, input) {
}
}
}
const noteData = Object.assign({},
const noteData = Object.assign(
{},
{
createdAt: new Date(),
updatedAt: new Date()
@@ -77,9 +84,13 @@ function createNote (storageKey, input) {
{
key,
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
})

View File

@@ -6,8 +6,12 @@ const createNote = require('./createNote')
import { push } from 'connected-react-router'
import ee from 'browser/main/lib/eventEmitter'
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)) {
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
)
) {
return true
} else {
return false
@@ -15,25 +19,33 @@ function validateUrl (str) {
}
const ERROR_MESSAGES = {
ENOTFOUND: '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',
ENOTFOUND:
'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!'
}
function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) {
function createNoteFromUrl(
url,
storage,
folder,
dispatch = null,
location = null
) {
return new Promise((resolve, reject) => {
const td = createTurndownService()
if (!validateUrl(url)) {
reject({result: false, error: ERROR_MESSAGES.VALIDATION_ERROR})
reject({ result: false, error: ERROR_MESSAGES.VALIDATION_ERROR })
}
const request = url.startsWith('https') ? https : http
const req = request.request(url, (res) => {
const req = request.request(url, res => {
let data = ''
res.on('data', (chunk) => {
res.on('data', chunk => {
data += chunk
})
@@ -46,20 +58,21 @@ function createNoteFromUrl (url, storage, folder, dispatch = null, location = nu
folder: folder,
title: '',
content: markdownHTML
})
.then((note) => {
}).then(note => {
const noteHash = note.key
dispatch({
type: 'UPDATE_NOTE',
note: note
})
dispatch(push({
pathname: location.pathname,
query: {key: noteHash}
}))
dispatch(
push({
pathname: location.pathname,
query: { key: noteHash }
})
)
ee.emit('list:jump', noteHash)
ee.emit('detail:focus')
resolve({result: true, error: null})
resolve({ result: true, error: null })
})
} else {
createNote(storage, {
@@ -67,16 +80,19 @@ function createNoteFromUrl (url, storage, folder, dispatch = null, location = nu
folder: folder,
title: '',
content: markdownHTML
}).then((note) => {
resolve({result: true, note, error: null})
}).then(note => {
resolve({ result: true, note, error: null })
})
}
})
})
req.on('error', (e) => {
req.on('error', 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()

View File

@@ -3,7 +3,7 @@ import crypto from 'crypto'
import consts from 'browser/lib/consts'
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
function createSnippet (snippetFile) {
function createSnippet(snippetFile) {
return new Promise((resolve, reject) => {
const newSnippet = {
id: crypto.randomBytes(16).toString('hex'),
@@ -12,15 +12,21 @@ function createSnippet (snippetFile) {
content: '',
linesHighlighted: []
}
fetchSnippet(null, snippetFile).then((snippets) => {
snippets.push(newSnippet)
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
if (err) reject(err)
resolve(newSnippet)
fetchSnippet(null, snippetFile)
.then(snippets => {
snippets.push(newSnippet)
fs.writeFile(
snippetFile || consts.SNIPPET_FILE,
JSON.stringify(snippets, null, 4),
err => {
if (err) reject(err)
resolve(newSnippet)
}
)
})
.catch(err => {
reject(err)
})
}).catch((err) => {
reject(err)
})
})
}

View File

@@ -18,7 +18,7 @@ const deleteSingleNote = require('./deleteNote')
* }
* ```
*/
function deleteFolder (storageKey, folderKey) {
function deleteFolder(storageKey, folderKey) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -27,35 +27,36 @@ function deleteFolder (storageKey, folderKey) {
}
return resolveStorageData(targetStorage)
.then(function assignNotes (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
return {
storage,
notes
}
})
.then(function assignNotes(storage) {
return resolveStorageNotes(storage).then(notes => {
return {
storage,
notes
}
})
})
.then(function deleteFolderAndNotes (data) {
.then(function deleteFolderAndNotes(data) {
const { storage, notes } = data
storage.folders = storage.folders
.filter(function excludeTargetFolder (folder) {
return folder.key !== folderKey
})
storage.folders = storage.folders.filter(function excludeTargetFolder(
folder
) {
return folder.key !== folderKey
})
const targetNotes = notes.filter(function filterTargetNotes (note) {
const targetNotes = notes.filter(function filterTargetNotes(note) {
return note.folder === folderKey
})
const deleteAllNotes = targetNotes
.map(function deleteNote (note) {
return deleteSingleNote(storageKey, note.key)
})
return Promise.all(deleteAllNotes)
.then(() => storage)
const deleteAllNotes = targetNotes.map(function deleteNote(note) {
return deleteSingleNote(storageKey, note.key)
})
return Promise.all(deleteAllNotes).then(() => storage)
})
.then(function (storage) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
.then(function(storage) {
CSON.writeFileSync(
path.join(storage.path, 'boostnote.json'),
_.pick(storage, ['folders', 'version'])
)
return {
storage,

View File

@@ -4,7 +4,7 @@ const sander = require('sander')
const attachmentManagement = require('./attachmentManagement')
const { findStorage } = require('browser/lib/findStorage')
function deleteNote (storageKey, noteKey) {
function deleteNote(storageKey, noteKey) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -13,7 +13,7 @@ function deleteNote (storageKey, noteKey) {
}
return resolveStorageData(targetStorage)
.then(function deleteNoteFile (storage) {
.then(function deleteNoteFile(storage) {
const notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
@@ -26,8 +26,11 @@ function deleteNote (storageKey, noteKey) {
storageKey
}
})
.then(function deleteAttachments (storageInfo) {
attachmentManagement.deleteAttachmentFolder(storageInfo.storageKey, storageInfo.noteKey)
.then(function deleteAttachments(storageInfo) {
attachmentManagement.deleteAttachmentFolder(
storageInfo.storageKey,
storageInfo.noteKey
)
return storageInfo
})
}

View File

@@ -2,14 +2,20 @@ import fs from 'fs'
import consts from 'browser/lib/consts'
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
function deleteSnippet (snippet, snippetFile) {
function deleteSnippet(snippet, snippetFile) {
return new Promise((resolve, reject) => {
fetchSnippet(null, snippetFile).then((snippets) => {
snippets = snippets.filter(currentSnippet => currentSnippet.id !== snippet.id)
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
if (err) reject(err)
resolve(snippet)
})
fetchSnippet(null, snippetFile).then(snippets => {
snippets = snippets.filter(
currentSnippet => currentSnippet.id !== snippet.id
)
fs.writeFile(
snippetFile || consts.SNIPPET_FILE,
JSON.stringify(snippets, null, 4),
err => {
if (err) reject(err)
resolve(snippet)
}
)
})
})
}

View File

@@ -22,7 +22,7 @@ import * as path from 'path'
* ```
*/
function exportFolder (storageKey, folderKey, fileType, exportDir) {
function exportFolder(storageKey, folderKey, fileType, exportDir) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -31,24 +31,38 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
}
return resolveStorageData(targetStorage)
.then(function assignNotes (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
return {
storage,
notes
}
})
.then(function assignNotes(storage) {
return resolveStorageNotes(storage).then(notes => {
return {
storage,
notes
}
})
})
.then(function exportNotes (data) {
.then(function exportNotes(data) {
const { storage, notes } = data
return Promise.all(notes
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
.map(note => {
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
return exportNote(note.key, storage.path, note.content, notePath, null)
})
return Promise.all(
notes
.filter(
note =>
note.folder === folderKey &&
note.isTrashed === false &&
note.type === 'MARKDOWN_NOTE'
)
.map(note => {
const notePath = path.join(
exportDir,
`${filenamify(note.title, { replacement: '_' })}.${fileType}`
)
return exportNote(
note.key,
storage.path,
note.content,
notePath,
null
)
})
).then(() => ({
storage,
folderKey,

View File

@@ -19,8 +19,16 @@ const attachmentManagement = require('./attachmentManagement')
* @param {function} outputFormatter
* @return {Promise.<*[]>}
*/
function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
function exportNote(
nodeKey,
storageKey,
noteContent,
targetPath,
outputFormatter
) {
const storagePath = path.isAbsolute(storageKey)
? storageKey
: findStorage(storageKey).path
const exportTasks = []
if (!storagePath) {
@@ -50,18 +58,19 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
.then(() => exportedData)
.then(data => {
return saveToFile(data, targetPath)
}).catch((err) => {
rollbackExport(tasks)
throw err
})
return Promise.all(tasks.map(task => copyFile(task.src, task.dst)))
.then(() => exportedData)
.then(data => {
return saveToFile(data, targetPath)
})
.catch(err => {
rollbackExport(tasks)
throw err
})
}
function prepareTasks (tasks, storagePath, targetPath) {
return tasks.map((task) => {
function prepareTasks(tasks, storagePath, targetPath) {
return tasks.map(task => {
if (!path.isAbsolute(task.src)) {
task.src = path.join(storagePath, task.src)
}
@@ -74,9 +83,9 @@ function prepareTasks (tasks, storagePath, targetPath) {
})
}
function saveToFile (data, filename) {
function saveToFile(data, filename) {
return new Promise((resolve, reject) => {
fs.writeFile(filename, data, (err) => {
fs.writeFile(filename, data, err => {
if (err) return reject(err)
resolve(filename)
@@ -88,9 +97,9 @@ function saveToFile (data, filename) {
* Remove exported files
* @param tasks Array of copy task objects. Object consists of two mandatory fields `src` and `dst`
*/
function rollbackExport (tasks) {
function rollbackExport(tasks) {
const folders = new Set()
tasks.forEach((task) => {
tasks.forEach(task => {
let fullpath = 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) {
fs.rmdir(folder)
}

View File

@@ -20,7 +20,7 @@ import * as fs from 'fs'
* ```
*/
function exportStorage (storageKey, fileType, exportDir) {
function exportStorage(storageKey, fileType, exportDir) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -29,14 +29,17 @@ function exportStorage (storageKey, fileType, exportDir) {
}
return resolveStorageData(targetStorage)
.then(storage => (
resolveStorageNotes(storage).then(notes => ({storage, notes}))
))
.then(function exportNotes (data) {
.then(storage =>
resolveStorageNotes(storage).then(notes => ({ storage, notes }))
)
.then(function exportNotes(data) {
const { storage, notes } = data
const folderNamesMapping = {}
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
// make sure directory exists
try {
@@ -47,7 +50,9 @@ function exportStorage (storageKey, fileType, exportDir) {
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
.forEach(markdownNote => {
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)
fs.writeFileSync(notePath, markdownNote.content)
})

View File

@@ -1,7 +1,7 @@
import fs from 'fs'
import consts from 'browser/lib/consts'
function fetchSnippet (id, snippetFile) {
function fetchSnippet(id, snippetFile) {
return new Promise((resolve, reject) => {
fs.readFile(snippetFile || consts.SNIPPET_FILE, 'utf8', (err, data) => {
if (err) {
@@ -9,7 +9,9 @@ function fetchSnippet (id, snippetFile) {
}
const snippets = JSON.parse(data)
if (id) {
const snippet = snippets.find(snippet => { return snippet.id === id })
const snippet = snippets.find(snippet => {
return snippet.id === id
})
resolve(snippet)
}
resolve(snippets)

View File

@@ -21,8 +21,8 @@ const CSON = require('@rokt33r/season')
* 3. empty directory
*/
function init () {
const fetchStorages = function () {
function init() {
const fetchStorages = function() {
let rawStorages
try {
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
@@ -34,44 +34,50 @@ function init () {
rawStorages = []
window.localStorage.setItem('storages', JSON.stringify(rawStorages))
}
return Promise.all(rawStorages
.map(resolveStorageData))
return Promise.all(rawStorages.map(resolveStorageData))
}
const fetchNotes = function (storages) {
const fetchNotes = function(storages) {
const findNotesFromEachStorage = storages
.filter(storage => fs.existsSync(storage.path))
.map((storage) => {
return resolveStorageNotes(storage)
.then((notes) => {
let unknownCount = 0
notes.forEach((note) => {
if (note && !storage.folders.some((folder) => note.folder === folder.key)) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
})
if (unknownCount > 0) {
try {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
} catch (e) {
console.log('Error writting boostnote.json: ' + e + ' from init.js')
}
.filter(storage => fs.existsSync(storage.path))
.map(storage => {
return resolveStorageNotes(storage).then(notes => {
let unknownCount = 0
notes.forEach(note => {
if (
note &&
!storage.folders.some(folder => note.folder === folder.key)
) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
return notes
})
if (unknownCount > 0) {
try {
CSON.writeFileSync(
path.join(storage.path, 'boostnote.json'),
_.pick(storage, ['folders', 'version'])
)
} catch (e) {
console.log(
'Error writting boostnote.json: ' + e + ' from init.js'
)
}
}
return notes
})
})
return Promise.all(findNotesFromEachStorage)
.then(function concatNoteGroup (noteGroups) {
return noteGroups.reduce(function (sum, group) {
.then(function concatNoteGroup(noteGroups) {
return noteGroups.reduce(function(sum, group) {
return sum.concat(group)
}, [])
})
.then(function returnData (notes) {
.then(function returnData(notes) {
return {
storages,
notes
@@ -80,12 +86,11 @@ function init () {
}
return Promise.resolve(fetchStorages())
.then((storages) => {
return storages
.filter((storage) => {
if (!_.isObject(storage)) return false
return true
})
.then(storages => {
return storages.filter(storage => {
if (!_.isObject(storage)) return false
return true
})
})
.then(fetchNotes)
}

View File

@@ -6,102 +6,111 @@ const CSON = require('@rokt33r/season')
const path = require('path')
const sander = require('sander')
function migrateFromV5Storage (storageKey, data) {
function migrateFromV5Storage(storageKey, data) {
let targetStorage
try {
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})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, { key: storageKey })
if (targetStorage == null) throw new Error("Target storage doesn't exist.")
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function (storage) {
return importAll(storage, data)
})
return resolveStorageData(targetStorage).then(function(storage) {
return importAll(storage, data)
})
}
function importAll (storage, data) {
function importAll(storage, data) {
const oldArticles = data.articles
const notes = []
data.folders
.forEach(function (oldFolder) {
let folderKey = keygen()
while (storage.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
const newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
data.folders.forEach(function(oldFolder) {
let folderKey = keygen()
while (storage.folders.some(folder => folder.key === folderKey)) {
folderKey = keygen()
}
const newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
storage.folders.push(newFolder)
storage.folders.push(newFolder)
const articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
articles.forEach((article) => {
let noteKey = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson'))
noteKey = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
console.error('Failed to read `notes` directory.')
throw err
}
const articles = oldArticles.filter(
article => article.FolderKey === oldFolder.key
)
articles.forEach(article => {
let noteKey = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson'))
noteKey = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
console.error('Failed to read `notes` directory.')
throw err
}
}
}
if (article.mode === 'markdown') {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'MARKDOWN_NOTE',
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey,
linesHighlighted: article.linesHighlighted
}
notes.push(newNote)
} else {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'SNIPPET_NOTE',
isStarred: false,
title: article.title,
description: article.title,
key: noteKey,
snippets: [{
if (article.mode === 'markdown') {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'MARKDOWN_NOTE',
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey,
linesHighlighted: article.linesHighlighted
}
notes.push(newNote)
} else {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'SNIPPET_NOTE',
isStarred: false,
title: article.title,
description: article.title,
key: noteKey,
snippets: [
{
name: article.mode,
mode: article.mode,
content: article.content,
linesHighlighted: article.linesHighlighted
}]
}
notes.push(newNote)
}
]
}
})
notes.push(newNote)
}
})
notes.forEach(function (note) {
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']))
notes.forEach(function(note) {
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'])
)
return {
storage,

Some files were not shown because too many files have changed in this diff Show More