diff --git a/.eslintrc b/.eslintrc index 1709c9d8..be8cb903 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,7 +18,9 @@ "globals": { "FileReader": true, "localStorage": true, - "fetch": true + "fetch": true, + "Image": true, + "MutationObserver": true }, "env": { "jest": true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4ec9b5bf..ea304082 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://issuehunt.io/r/BoostIo/Boostnote +issuehunt: BoostIo/Boostnote diff --git a/.gitignore b/.gitignore index ace5316c..aac64950 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ node_modules/* /secret *.log .idea -.vscode \ No newline at end of file +.vscode +package-lock.json diff --git a/.travis.yml b/.travis.yml index 90548ee9..71b65671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ node_js: - 8 script: - npm run lint && npm run test - - yarn jest - 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@6.4 && grunt pre-build; fi' after_success: - openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 1abd15a9..9214363e 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -20,14 +20,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') -import TurndownService from 'turndown' +const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu +import { createTurndownService } from '../lib/turndown' import {languageMaps} from '../lib/CMLanguageList' import snippetManager from '../lib/SnippetManager' import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator' import markdownlint from 'markdownlint' import Jsonlint from 'jsonlint-mod' import { DEFAULT_CONFIG } from '../main/lib/ConfigManager' +import prettier from 'prettier' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -53,6 +54,7 @@ export default class CodeEditor extends React.Component { this.focusHandler = () => { ipcRenderer.send('editor:focused', true) } + const debouncedDeletionOfAttachments = _.debounce(attachmentManagement.deleteAttachmentsNotPresentInNote, 30000) this.blurHandler = (editor, e) => { ipcRenderer.send('editor:focused', false) if (e == null) return null @@ -64,16 +66,13 @@ export default class CodeEditor extends React.Component { el = el.parentNode } this.props.onBlur != null && this.props.onBlur(e) - const { storageKey, noteKey } = this.props - attachmentManagement.deleteAttachmentsNotPresentInNote( - this.editor.getValue(), - storageKey, - noteKey - ) + if (this.props.deleteUnusedAttachments === true) { + debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey) + } } this.pasteHandler = (editor, e) => { e.preventDefault() @@ -102,7 +101,7 @@ export default class CodeEditor extends React.Component { this.editorActivityHandler = () => this.handleEditorActivity() - this.turndownService = new TurndownService() + this.turndownService = createTurndownService() } handleSearch (msg) { @@ -110,7 +109,7 @@ export default class CodeEditor extends React.Component { const component = this if (component.searchState) cm.removeOverlay(component.searchState) - if (msg.length < 3) return + if (msg.length < 1) return cm.operation(function () { component.searchState = makeOverlay(msg, 'searching') @@ -205,23 +204,11 @@ export default class CodeEditor extends React.Component { 'Cmd-T': function (cm) { // Do nothing }, - 'Ctrl-/': function (cm) { - if (global.process.platform === 'darwin') { return } + [translateHotkey(hotkey.insertDate)]: function (cm) { const dateNow = new Date() cm.replaceSelection(dateNow.toLocaleDateString()) }, - 'Cmd-/': function (cm) { - if (global.process.platform !== 'darwin') { return } - const dateNow = new Date() - cm.replaceSelection(dateNow.toLocaleDateString()) - }, - 'Shift-Ctrl-/': function (cm) { - if (global.process.platform === 'darwin') { return } - const dateNow = new Date() - cm.replaceSelection(dateNow.toLocaleString()) - }, - 'Shift-Cmd-/': function (cm) { - if (global.process.platform !== 'darwin') { return } + [translateHotkey(hotkey.insertDateTime)]: function (cm) { const dateNow = new Date() cm.replaceSelection(dateNow.toLocaleString()) }, @@ -232,6 +219,37 @@ export default class CodeEditor extends React.Component { } return CodeMirror.Pass }, + [translateHotkey(hotkey.prettifyMarkdown)]: cm => { + // Default / User configured prettier options + const currentConfig = JSON.parse(self.props.prettierConfig) + + // Parser type will always need to be markdown so we override the option before use + currentConfig.parser = 'markdown' + + // Get current cursor position + const cursorPos = cm.getCursor() + currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos) + + // Prettify contents of editor + const formattedTextDetails = prettier.formatWithCursor(cm.doc.getValue(), currentConfig) + + const formattedText = formattedTextDetails.formatted + const formattedCursorPos = formattedTextDetails.cursorOffset + cm.doc.setValue(formattedText) + + // Reset Cursor position to be at the same markdown as was before prettifying + const newCursorPos = cm.doc.posFromIndex(formattedCursorPos) + cm.doc.setCursor(newCursorPos) + }, + [translateHotkey(hotkey.sortLines)]: cm => { + const selection = cm.doc.getSelection() + const appendLineBreak = /\n$/.test(selection) + + const sorted = _.split(selection.trim(), '\n').sort() + const sortedString = _.join(sorted, '\n') + (appendLineBreak ? '\n' : '') + + cm.doc.replaceSelection(sortedString) + }, [translateHotkey(hotkey.pasteSmartly)]: cm => { this.handlePaste(cm, true) } @@ -267,7 +285,7 @@ export default class CodeEditor extends React.Component { value: this.props.value, linesHighlighted: this.props.linesHighlighted, lineNumbers: this.props.displayLineNumbers, - lineWrapping: true, + lineWrapping: this.props.lineWrapping, theme: this.props.theme, indentUnit: this.props.indentSize, tabSize: this.props.indentSize, @@ -285,7 +303,8 @@ export default class CodeEditor extends React.Component { explode: this.props.explodingPairs, override: true }, - extraKeys: this.defaultKeyMap + extraKeys: this.defaultKeyMap, + prettierConfig: this.props.prettierConfig }) document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none' @@ -566,6 +585,10 @@ export default class CodeEditor extends React.Component { this.editor.setOption('lineNumbers', this.props.displayLineNumbers) } + if (prevProps.lineWrapping !== this.props.lineWrapping) { + this.editor.setOption('lineWrapping', this.props.lineWrapping) + } + if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) { this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) } @@ -620,6 +643,9 @@ export default class CodeEditor extends React.Component { this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) } } + if (prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments) { + this.editor.setOption('deleteUnusedAttachments', this.props.deleteUnusedAttachments) + } if (needRefresh) { this.editor.refresh() @@ -848,6 +874,17 @@ export default class CodeEditor extends React.Component { this.editor.setCursor(cursor) } + /** + * Update content of one line + * @param {Number} lineNumber + * @param {String} 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 }) + } + handleDropImage (dropEvent) { dropEvent.preventDefault() const { @@ -1181,7 +1218,8 @@ CodeEditor.propTypes = { autoDetect: PropTypes.bool, spellCheck: PropTypes.bool, enableMarkdownLint: PropTypes.bool, - customMarkdownLintConfig: PropTypes.string + customMarkdownLintConfig: PropTypes.string, + deleteUnusedAttachments: PropTypes.bool } CodeEditor.defaultProps = { @@ -1195,5 +1233,7 @@ CodeEditor.defaultProps = { autoDetect: false, spellCheck: false, enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint, - customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig + customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig, + prettierConfig: DEFAULT_CONFIG.editor.prettierConfig, + deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments } diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 526ecb39..cd885fd9 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -159,24 +159,25 @@ class MarkdownEditor extends React.Component { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ - const checkedMatch = /^\s*[\+\-\*] \[x\]/i - const uncheckedMatch = /^\s*[\+\-\*] \[ \]/ - const checkReplace = /\[x\]/i - const uncheckReplace = /\[ \]/ + const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i + const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/ + const checkReplace = /\[x]/i + const uncheckReplace = /\[ ]/ if (idMatch.test(e.target.getAttribute('id'))) { const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lines = this.refs.code.value .split('\n') const targetLine = lines[lineIndex] + let newLine = targetLine if (targetLine.match(checkedMatch)) { - lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') + newLine = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { - lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') + newLine = targetLine.replace(uncheckReplace, '[x]') } - this.refs.code.setValue(lines.join('\n')) + this.refs.code.setLineContent(lineIndex, newLine) } } @@ -304,6 +305,7 @@ class MarkdownEditor extends React.Component { enableRulers={config.editor.enableRulers} rulers={config.editor.rulers} displayLineNumbers={config.editor.displayLineNumbers} + lineWrapping matchingPairs={config.editor.matchingPairs} matchingTriples={config.editor.matchingTriples} explodingPairs={config.editor.explodingPairs} @@ -321,6 +323,8 @@ class MarkdownEditor extends React.Component { switchPreview={config.editor.switchPreview} enableMarkdownLint={config.editor.enableMarkdownLint} customMarkdownLintConfig={config.editor.customMarkdownLintConfig} + prettierConfig={config.editor.prettierConfig} + deleteUnusedAttachments={config.editor.deleteUnusedAttachments} /> this.handleContextMenu(e)} onDoubleClick={(e) => this.handleDoubleClick(e)} diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index ee573c6e..879021ef 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -19,9 +19,6 @@ import mdurl from 'mdurl' import exportNote from 'browser/main/lib/dataApi/exportNote' import { escapeHtmlCharacters } from 'browser/lib/utils' import yaml from 'js-yaml' -import context from 'browser/lib/context' -import i18n from 'browser/lib/i18n' -import fs from 'fs' import { render } from 'react-dom' import Carousel from 'react-image-carousel' import { push } from 'connected-react-router' @@ -29,6 +26,7 @@ import ConfigManager from '../main/lib/ConfigManager' const { remote, shell } = require('electron') const attachmentManagement = require('../main/lib/dataApi/attachmentManagement') +const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu const { app } = remote const path = require('path') @@ -36,8 +34,6 @@ const fileUrl = require('file-url') const dialog = remote.dialog -const uri2path = require('file-uri-to-path') - const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] const appPath = fileUrl( process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve() @@ -48,16 +44,31 @@ const CSS_FILES = [ `${appPath}/node_modules/react-image-carousel/lib/css/main.min.css` ] -function buildStyle ( - fontFamily, - fontSize, - codeBlockFontFamily, - lineNumber, - scrollPastEnd, - theme, - allowCustomCSS, - customCSS -) { +/** + * @param {Object} opts + * @param {String} opts.fontFamily + * @param {Numberl} opts.fontSize + * @param {String} opts.codeBlockFontFamily + * @param {String} opts.theme + * @param {Boolean} [opts.lineNumber] Should show line number + * @param {Boolean} [opts.scrollPastEnd] + * @param {Boolean} [opts.optimizeOverflowScroll] Should tweak body style to optimize overflow scrollbar display + * @param {Boolean} [opts.allowCustomCSS] Should add custom css + * @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy + * @returns {String} + */ +function buildStyle (opts) { + const { + fontFamily, + fontSize, + codeBlockFontFamily, + lineNumber, + scrollPastEnd, + optimizeOverflowScroll, + theme, + allowCustomCSS, + customCSS + } = opts return ` @font-face { font-family: 'Lato'; @@ -87,12 +98,14 @@ function buildStyle ( url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'), url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype'); } + ${markdownStyle} body { font-family: '${fontFamily.join("','")}'; font-size: ${fontSize}px; - ${scrollPastEnd && 'padding-bottom: 90vh;'} + ${scrollPastEnd ? 'padding-bottom: 90vh;' : ''} + ${optimizeOverflowScroll ? 'height: 100%;' : ''} } @media print { body { @@ -251,30 +264,12 @@ class MarkdownPreview extends React.Component { } handleContextMenu (event) { - // If a contextMenu handler was passed to us, use it instead of the self-defined one -> return - if (_.isFunction(this.props.onContextMenu)) { + const menu = buildMarkdownPreviewContextMenu(this, event) + const switchPreview = ConfigManager.get().editor.switchPreview + if (menu != null && switchPreview !== 'RIGHTCLICK') { + menu.popup(remote.getCurrentWindow()) + } else if (_.isFunction(this.props.onContextMenu)) { this.props.onContextMenu(event) - return - } - // No contextMenu was passed to us -> execute our own link-opener - if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) { - const href = event.target.href - const isLocalFile = href.startsWith('file:') - if (isLocalFile) { - const absPath = uri2path(href) - try { - if (fs.lstatSync(absPath).isFile()) { - context.popup([ - { - label: i18n.__('Show in explorer'), - click: (e) => shell.showItemInFolder(absPath) - } - ]) - } - } catch (e) { - console.log('Error while evaluating if the file is locally available', e) - } - } } } @@ -336,7 +331,7 @@ class MarkdownPreview extends React.Component { customCSS } = this.getStyleParams() - const inlineStyles = buildStyle( + const inlineStyles = buildStyle({ fontFamily, fontSize, codeBlockFontFamily, @@ -345,9 +340,13 @@ class MarkdownPreview extends React.Component { theme, allowCustomCSS, customCSS - ) + }) let body = this.markdown.render(noteContent) - const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] + body = attachmentManagement.fixLocalURLS( + body, + this.props.storagePath + ) + const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES] files.forEach(file => { if (global.process.platform === 'win32') { file = file.replace('file:///', '') @@ -383,7 +382,7 @@ class MarkdownPreview extends React.Component { handleSaveAsPdf () { this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => { - const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false}}) + 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', () => { @@ -578,16 +577,19 @@ class MarkdownPreview extends React.Component { } componentDidUpdate (prevProps) { - if (prevProps.value !== this.props.value) this.rewriteIframe() + // actual rewriteIframe function should be called only once + let needsRewriteIframe = false + if (prevProps.value !== this.props.value) needsRewriteIframe = true if ( prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize || + prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel || prevProps.smartArrows !== this.props.smartArrows || prevProps.breaks !== this.props.breaks || prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox ) { this.initMarkdown() - this.rewriteIframe() + needsRewriteIframe = true } if ( prevProps.fontFamily !== this.props.fontFamily || @@ -602,8 +604,17 @@ class MarkdownPreview extends React.Component { prevProps.customCSS !== this.props.customCSS ) { this.applyStyle() + needsRewriteIframe = true + } + + if (needsRewriteIframe) { this.rewriteIframe() } + + // Should scroll to top after selecting another note + if (prevProps.noteKey !== this.props.noteKey) { + this.getWindow().scrollTo(0, 0) + } } getStyleParams () { @@ -659,27 +670,27 @@ class MarkdownPreview extends React.Component { this.getWindow().document.getElementById( 'codeTheme' - ).href = this.GetCodeThemeLink(codeBlockTheme) - this.getWindow().document.getElementById('style').innerHTML = buildStyle( + ).href = this.getCodeThemeLink(codeBlockTheme) + this.getWindow().document.getElementById('style').innerHTML = buildStyle({ fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, + optimizeOverflowScroll: true, theme, allowCustomCSS, customCSS - ) + }) + this.getWindow().document.documentElement.style.overflowY = 'hidden' } - GetCodeThemeLink (name) { + getCodeThemeLink (name) { const theme = consts.THEMES.find(theme => theme.name === name) - if (theme) { - return `${appPath}/${theme.path}` - } else { - return `${appPath}/node_modules/codemirror/theme/elegant.css` - } + return theme != null + ? theme.path + : `${appPath}/node_modules/codemirror/theme/elegant.css` } rewriteIframe () { @@ -705,7 +716,8 @@ class MarkdownPreview extends React.Component { showCopyNotification, storagePath, noteKey, - sanitize + sanitize, + mermaidHTMLLabel } = this.props let { value, codeBlockTheme } = this.props @@ -837,6 +849,7 @@ class MarkdownPreview extends React.Component { canvas.height = height.value + 'vh' } + // eslint-disable-next-line no-unused-vars const chart = new Chart(canvas, chartConfig) } catch (e) { el.className = 'chart-error' @@ -847,7 +860,7 @@ class MarkdownPreview extends React.Component { _.forEach( this.refs.root.contentWindow.document.querySelectorAll('.mermaid'), el => { - mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme) + mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme, mermaidHTMLLabel) } ) @@ -897,6 +910,12 @@ class MarkdownPreview extends React.Component { this.setImgOnClickEventHelper(img, rect) imgObserver.observe(parentEl, config) } + + 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) { @@ -969,8 +988,6 @@ class MarkdownPreview extends React.Component { overlay.appendChild(zoomImg) document.body.appendChild(overlay) } - - this.getWindow().scrollTo(0, 0) } focus () { @@ -1018,26 +1035,34 @@ class MarkdownPreview extends React.Component { e.stopPropagation() const rawHref = e.target.getAttribute('href') - const parser = document.createElement('a') - parser.href = e.target.getAttribute('href') - const { href, hash } = parser - const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10 const { dispatch } = this.props + if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() + + const parser = document.createElement('a') + parser.href = rawHref + const isStartWithHash = rawHref[0] === '#' + const { href, hash } = parser if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() - const regexNoteInternalLink = /.*[main.\w]*.html#/ + const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10 - if (regexNoteInternalLink.test(href)) { - const targetId = mdurl.encode(linkHash) - const targetElement = this.refs.root.contentWindow.document.querySelector( - targetId - ) + const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html + const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`) + if (isStartWithHash || regexNoteInternalLink.test(rawHref)) { + const posOfHash = linkHash.indexOf('#') + if (posOfHash > -1) { + const extractedId = linkHash.slice(posOfHash + 1) + const targetId = mdurl.encode(extractedId) + const targetElement = this.refs.root.contentWindow.document.getElementById( + targetId + ) - if (targetElement != null) { - this.getWindow().scrollTo(0, targetElement.offsetTop) + if (targetElement != null) { + this.getWindow().scrollTo(0, targetElement.offsetTop) + } + return } - return } // this will match the new uuid v4 hash and the old hash diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 874ce4be..f5996c59 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -78,24 +78,25 @@ class MarkdownSplitEditor extends React.Component { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ - const checkedMatch = /^\s*[\+\-\*] \[x\]/i - const uncheckedMatch = /^\s*[\+\-\*] \[ \]/ - const checkReplace = /\[x\]/i - const uncheckReplace = /\[ \]/ + const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i + const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/ + const checkReplace = /\[x]/i + const uncheckReplace = /\[ ]/ if (idMatch.test(e.target.getAttribute('id'))) { const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lines = this.refs.code.value .split('\n') const targetLine = lines[lineIndex] + let newLine = targetLine if (targetLine.match(checkedMatch)) { - lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') + newLine = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { - lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') + newLine = targetLine.replace(uncheckReplace, '[x]') } - this.refs.code.setValue(lines.join('\n')) + this.refs.code.setLineContent(lineIndex, newLine) } } @@ -150,7 +151,6 @@ class MarkdownSplitEditor extends React.Component { onMouseMove={e => this.handleMouseMove(e)} onMouseUp={e => this.handleMouseUp(e)}>
this.handleMouseDown(e)} >
( ) diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 625bb38d..168af1ff 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -3,7 +3,7 @@ */ import PropTypes from 'prop-types' import React from 'react' -import { isArray } from 'lodash' +import { isArray, sortBy } from 'lodash' import invertColor from 'invert-color' import CSSModules from 'browser/lib/CSSModules' import { getTodoStatus } from 'browser/lib/getTodoStatus' @@ -43,7 +43,7 @@ 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] })) } @@ -148,15 +148,14 @@ NoteItem.propTypes = { tags: PropTypes.array, isStarred: PropTypes.bool.isRequired, isTrashed: PropTypes.bool.isRequired, - blog: { + blog: PropTypes.shape({ blogLink: PropTypes.string, blogId: PropTypes.number - } + }) }), handleNoteClick: PropTypes.func.isRequired, handleNoteContextMenu: PropTypes.func.isRequired, - handleDragStart: PropTypes.func.isRequired, - handleDragEnd: PropTypes.func.isRequired + handleDragStart: PropTypes.func.isRequired } export default CSSModules(NoteItem, styles) diff --git a/browser/components/SideNavFilter.js b/browser/components/SideNavFilter.js index 3a259ce7..5d5d627f 100644 --- a/browser/components/SideNavFilter.js +++ b/browser/components/SideNavFilter.js @@ -74,7 +74,7 @@ SideNavFilter.propTypes = { isStarredActive: PropTypes.bool.isRequired, isTrashedActive: PropTypes.bool.isRequired, handleStarredButtonClick: PropTypes.func.isRequired, - handleTrashdButtonClick: PropTypes.func.isRequired + handleTrashedButtonClick: PropTypes.func.isRequired } export default CSSModules(SideNavFilter, styles) diff --git a/browser/components/SnippetTab.js b/browser/components/SnippetTab.js index c030351f..d29130c7 100644 --- a/browser/components/SnippetTab.js +++ b/browser/components/SnippetTab.js @@ -114,7 +114,7 @@ class SnippetTab extends React.Component { > {snippet.name.trim().length > 0 ? snippet.name - : + : {i18n.__('Unnamed')} } diff --git a/browser/components/TodoProcess.js b/browser/components/TodoProcess.js index 251fd5b9..9d1f93cf 100644 --- a/browser/components/TodoProcess.js +++ b/browser/components/TodoProcess.js @@ -25,10 +25,10 @@ const TodoProcess = ({ ) TodoProcess.propTypes = { - todoStatus: { + todoStatus: PropTypes.exact({ total: PropTypes.number.isRequired, completed: PropTypes.number.isRequired - } + }) } export default CSSModules(TodoProcess, styles) diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js index e28e06ea..d9ea549b 100644 --- a/browser/components/render/MermaidRender.js +++ b/browser/components/render/MermaidRender.js @@ -19,7 +19,7 @@ function getId () { return id } -function render (element, content, theme) { +function render (element, content, theme, enableHTMLLabel) { try { const height = element.attributes.getNamedItem('data-height') if (height && height.value !== 'undefined') { @@ -29,7 +29,8 @@ function render (element, content, theme) { mermaidAPI.initialize({ theme: isDarkTheme ? 'dark' : 'default', themeCSS: isDarkTheme ? darkThemeStyling : '', - useMaxWidth: false + useMaxWidth: false, + flowchart: { htmlLabels: enableHTMLLabel } }) mermaidAPI.render(getId(), content, (svgGraph) => { element.innerHTML = svgGraph diff --git a/browser/lib/consts.js b/browser/lib/consts.js index 9c993055..ed497376 100644 --- a/browser/lib/consts.js +++ b/browser/lib/consts.js @@ -7,6 +7,7 @@ const CODEMIRROR_THEME_PATH = 'node_modules/codemirror/theme' const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme' const isProduction = process.env.NODE_ENV === 'production' + const paths = [ isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH), isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH) @@ -18,7 +19,7 @@ const themes = paths return { name, - path: path.join(directory.split(/\//g).slice(-3).join('/'), file), + path: path.join(directory, file), className: `cm-s-${name}` } })) @@ -27,17 +28,16 @@ const themes = paths themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, { name: 'solarized dark', - path: `${CODEMIRROR_THEME_PATH}/solarized.css`, + path: path.join(paths[0], 'solarized.css'), className: `cm-s-solarized cm-s-dark` }, { name: 'solarized light', - path: `${CODEMIRROR_THEME_PATH}/solarized.css`, + path: path.join(paths[0], 'solarized.css'), className: `cm-s-solarized cm-s-light` }) - themes.splice(0, 0, { name: 'default', - path: `${CODEMIRROR_THEME_PATH}/elegant.css`, + path: path.join(paths[0], 'elegant.css'), className: `cm-s-default` }) diff --git a/browser/lib/contextMenuBuilder.js b/browser/lib/contextMenuBuilder.js index cf92f52e..ff3349eb 100644 --- a/browser/lib/contextMenuBuilder.js +++ b/browser/lib/contextMenuBuilder.js @@ -1,6 +1,12 @@ +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 spellcheck = require('./spellcheck') +const uri2path = require('file-uri-to-path') /** * Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note. @@ -62,4 +68,57 @@ const buildEditorContextMenu = function (editor, event) { return Menu.buildFromTemplate(template) } -module.exports = buildEditorContextMenu +/** + * Creates the context menu that is shown when there is a right click Markdown preview of a (not-snippet) note. + * @param {MarkdownPreview} markdownPreview + * @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) { + return null + } + + // Default context menu inclusions + const template = [{ + role: 'copy' + }, { + role: 'selectall' + }] + + 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:') + if (isLocalFile) { + const absPath = uri2path(href) + try { + if (fs.lstatSync(absPath).isFile()) { + 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) + } + } + + // Add option to context menu to copy url + template.push( + { + label: i18n.__('Copy Url'), + click: (e) => clipboard.writeText(href) + } + ) + } + return Menu.buildFromTemplate(template) +} + +module.exports = +{ + buildEditorContextMenu: buildEditorContextMenu, + buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu +} diff --git a/browser/lib/customMeta.js b/browser/lib/customMeta.js index 0d4ee1e3..b890cf55 100644 --- a/browser/lib/customMeta.js +++ b/browser/lib/customMeta.js @@ -1,5 +1,10 @@ import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' -CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']}) +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']}) +} else { + stylusCodeInfo.alias = ['styl'] +} CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']}) diff --git a/browser/lib/getTodoStatus.js b/browser/lib/getTodoStatus.js index ab0d7809..8b552109 100644 --- a/browser/lib/getTodoStatus.js +++ b/browser/lib/getTodoStatus.js @@ -4,11 +4,11 @@ export function getTodoStatus (content) { let numberOfCompletedTodo = 0 splitted.forEach((line) => { - const trimmedLine = line.trim() - if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) { + const trimmedLine = line.trim().replace(/^(>\s*)*/, '') + if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) { numberOfTodo++ } - if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) { + if (trimmedLine.match(/^[+\-*] \[x] ./i)) { numberOfCompletedTodo++ } }) diff --git a/browser/lib/keygen.js b/browser/lib/keygen.js index 814efedd..557a8a40 100644 --- a/browser/lib/keygen.js +++ b/browser/lib/keygen.js @@ -1,5 +1,4 @@ const crypto = require('crypto') -const _ = require('lodash') const uuidv4 = require('uuid/v4') module.exports = function (uuid) { diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index 8f6d86a8..641216e3 100644 --- a/browser/lib/markdown-it-sanitize-html.js +++ b/browser/lib/markdown-it-sanitize-html.js @@ -15,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) { options ) } - if (state.tokens[tokenIdx].type === '_fence') { + if (state.tokens[tokenIdx].type.match(/.*_fence$/)) { // escapeHtmlCharacters has better performance state.tokens[tokenIdx].content = escapeHtmlCharacters( state.tokens[tokenIdx].content, @@ -96,6 +96,10 @@ function sanitizeInline (html, options) { function naughtyHRef (href, options) { // href = href.replace(/[\x00-\x20]+/g, '') + if (!href) { + // No href + return false + } href = href.replace(/<\!\-\-.*?\-\-\>/g, '') const matches = href.match(/^([a-zA-Z]+)\:/) diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js index 8f027247..7c76c1f3 100644 --- a/browser/lib/markdown-toc-generator.js +++ b/browser/lib/markdown-toc-generator.js @@ -21,7 +21,7 @@ function uniqueSlug (slug, slugs, opts) { } function linkify (token) { - token.content = mdlink(token.content, '#' + token.slug) + token.content = mdlink(token.content, `#${decodeURI(token.slug)}`) return token } diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index df429b19..57f981db 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -2,7 +2,9 @@ import markdownit from 'markdown-it' import sanitize from './markdown-it-sanitize-html' import emoji from 'markdown-it-emoji' import math from '@rokt33r/markdown-it-math' +import mdurl from 'mdurl' import smartArrows from 'markdown-it-smartarrows' +import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor' import _ from 'lodash' import ConfigManager from 'browser/main/lib/ConfigManager' import katex from 'katex' @@ -32,6 +34,7 @@ class Markdown { const updatedOptions = Object.assign(defaultOptions, options) this.md = markdownit(updatedOptions) + this.md.linkify.set({ fuzzyLink: false }) if (updatedOptions.sanitize !== 'NONE') { const allowedTags = ['iframe', 'input', 'b', @@ -125,6 +128,12 @@ class Markdown { this.md.use(require('markdown-it-abbr')) this.md.use(require('markdown-it-sub')) this.md.use(require('markdown-it-sup')) + this.md.use(markdownItTocAndAnchor, { + toc: true, + tocPattern: /\[TOC\]/i, + anchorLink: false, + appendIdToHeading: false + }) this.md.use(require('./markdown-it-deflist')) this.md.use(require('./markdown-it-frontmatter')) @@ -149,9 +158,9 @@ class Markdown { const content = token.content.split('\n').slice(0, -1).map(line => { const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line) if (match) { - return match[1] + return mdurl.encode(match[1]) } else { - return line + return mdurl.encode(line) } }).join('\n') @@ -181,32 +190,47 @@ class Markdown { }) const deflate = require('markdown-it-plantuml/lib/deflate') - this.md.use(require('markdown-it-plantuml'), { - generateSource: function (umlCode) { - const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url - const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg' - const s = unescape(encodeURIComponent(umlCode)) - const zippedCode = deflate.encode64( - deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9) - ) - return `${serverAddress}/${zippedCode}` - } + 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 s = unescape(encodeURIComponent(umlCode)) + const zippedCode = deflate.encode64( + deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9) + ) + return `${plantUmlServerAddress}/${type}/${zippedCode}` + } + + this.md.use(plantuml, { + generateSource: (umlCode) => parsePlantUml(umlCode, '@startuml', '@enduml', 'svg') }) - // Ditaa support - this.md.use(require('markdown-it-plantuml'), { + // 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: function (umlCode) { - const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url - // Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment. - const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png' - const s = unescape(encodeURIComponent(umlCode)) - const zippedCode = deflate.encode64( - deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9) - ) - return `${serverAddress}/${zippedCode}` - } + generateSource: (umlCode) => parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png') + }) + + // Mindmap support + this.md.use(plantuml, { + openMarker: '@startmindmap', + closeMarker: '@endmindmap', + generateSource: (umlCode) => parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg') + }) + + // WBS support + this.md.use(plantuml, { + openMarker: '@startwbs', + closeMarker: '@endwbs', + generateSource: (umlCode) => parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg') + }) + + // Gantt support + this.md.use(plantuml, { + openMarker: '@startgantt', + closeMarker: '@endgantt', + generateSource: (umlCode) => parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg') }) // Override task item @@ -287,7 +311,9 @@ class Markdown { case 'list_item_open': case 'paragraph_open': case 'table_open': - token.attrPush(['data-line', token.map[0]]) + if (token.map) { + token.attrPush(['data-line', token.map[0]]) + } } }) const result = originalRender.call(this.md.renderer, tokens, options, env) diff --git a/browser/lib/slugify.js b/browser/lib/slugify.js index a3447a90..21c18e02 100644 --- a/browser/lib/slugify.js +++ b/browser/lib/slugify.js @@ -1,17 +1,11 @@ -import diacritics from 'diacritics-map' - -function replaceDiacritics (str) { - return str.replace(/[À-ž]/g, function (ch) { - return diacritics[ch] || ch - }) -} - module.exports = function slugify (title) { - let slug = title.trim() + const slug = encodeURI( + title.trim() + .replace(/^\s+/, '') + .replace(/\s+$/, '') + .replace(/\s+/g, '-') + .replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g, '') + ) - slug = replaceDiacritics(slug) - - slug = slug.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-') - - return encodeURI(slug).replace(/\-+$/, '') + return slug } diff --git a/browser/lib/turndown.js b/browser/lib/turndown.js new file mode 100644 index 00000000..a1c3e128 --- /dev/null +++ b/browser/lib/turndown.js @@ -0,0 +1,9 @@ +const TurndownService = require('turndown') +const { gfm } = require('turndown-plugin-gfm') + +export const createTurndownService = function () { + const turndown = new TurndownService() + turndown.use(gfm) + turndown.remove('script') + return turndown +} diff --git a/browser/lib/utils.js b/browser/lib/utils.js index 4bcc9698..9f6f1425 100644 --- a/browser/lib/utils.js +++ b/browser/lib/utils.js @@ -136,9 +136,24 @@ export function isMarkdownTitleURL (str) { return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str) } +export function humanFileSize (bytes) { + const threshold = 1000 + if (Math.abs(bytes) < threshold) { + return bytes + ' B' + } + var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + var u = -1 + do { + bytes /= threshold + ++u + } while (Math.abs(bytes) >= threshold && u < units.length - 1) + return bytes.toFixed(1) + ' ' + units[u] +} + export default { lastFindInArray, escapeHtmlCharacters, isObjectEqual, - isMarkdownTitleURL + isMarkdownTitleURL, + humanFileSize } diff --git a/browser/main/Detail/FromUrlButton.js b/browser/main/Detail/FromUrlButton.js new file mode 100644 index 00000000..0d1c1b4c --- /dev/null +++ b/browser/main/Detail/FromUrlButton.js @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './FromUrlButton.styl' +import _ from 'lodash' +import i18n from 'browser/lib/i18n' + +class FromUrlButton extends React.Component { + constructor (props) { + super(props) + + this.state = { + isActive: false + } + } + + handleMouseDown (e) { + this.setState({ + isActive: true + }) + } + + handleMouseUp (e) { + this.setState({ + isActive: false + }) + } + + handleMouseLeave (e) { + this.setState({ + isActive: false + }) + } + + render () { + const { className } = this.props + + return ( + + ) + } +} + +FromUrlButton.propTypes = { + isActive: PropTypes.bool, + onClick: PropTypes.func, + className: PropTypes.string +} + +export default CSSModules(FromUrlButton, styles) diff --git a/browser/main/Detail/FromUrlButton.styl b/browser/main/Detail/FromUrlButton.styl new file mode 100644 index 00000000..66c2d730 --- /dev/null +++ b/browser/main/Detail/FromUrlButton.styl @@ -0,0 +1,41 @@ +.root + top 45px + topBarButtonRight() + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) + &:hover .tooltip + opacity 1 + +.tooltip + tooltip() + position absolute + pointer-events none + top 50px + right 125px + width 90px + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s + +.root--active + @extend .root + transition 0.15s + color $ui-favorite-star-button-color + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) + +.icon + transition transform 0.15s + height 13px + +body[data-theme="dark"] + .root + topBarButtonDark() + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) diff --git a/browser/main/Detail/FullscreenButton.js b/browser/main/Detail/FullscreenButton.js index bd76447c..eb33165f 100644 --- a/browser/main/Detail/FullscreenButton.js +++ b/browser/main/Detail/FullscreenButton.js @@ -11,7 +11,7 @@ const FullscreenButton = ({ const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B' return ( ) diff --git a/browser/main/Detail/InfoPanel.js b/browser/main/Detail/InfoPanel.js index 8fe0a855..86b5ae86 100644 --- a/browser/main/Detail/InfoPanel.js +++ b/browser/main/Detail/InfoPanel.js @@ -60,7 +60,7 @@ class InfoPanel extends React.Component {
- { e.target.select() }} /> + { e.target.select() }} /> diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 06fb91a5..207e1e2b 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -67,9 +67,6 @@ class MarkdownNoteDetail extends React.Component { }) ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this)) ee.on('code:generate-toc', this.generateToc) - - // Focus content if using blur or double click - if (this.state.switchPreview === 'BLUR' || this.state.switchPreview === 'DBL_CLICK') this.focus() } componentWillReceiveProps (nextProps) { @@ -84,6 +81,20 @@ class MarkdownNoteDetail extends React.Component { 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 + + if (this.state.switchPreview !== switchPreview) { + this.setState({ + switchPreview + }) + if (switchPreview === 'BLUR' || switchPreview === 'DBL_CLICK') { + console.log('setting focus', switchPreview) + this.focus() + } + } } componentWillUnmount () { @@ -141,7 +152,6 @@ class MarkdownNoteDetail extends React.Component { } handleFolderChange (e) { - const { dispatch } = this.props const { note } = this.state const value = this.refs.folder.value const splitted = value.split('-') @@ -300,7 +310,7 @@ class MarkdownNoteDetail extends React.Component { } getToggleLockButton () { - return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg' + return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg' } handleDeleteKeyDown (e) { @@ -399,7 +409,7 @@ class MarkdownNoteDetail extends React.Component { } render () { - const { data, location, config } = this.props + const { data, dispatch, location, config } = this.props const { note, editorType } = this.state const storageKey = note.storage const folderKey = note.folder @@ -439,7 +449,7 @@ class MarkdownNoteDetail extends React.Component { const detailTopBar =
-
+
@@ -461,6 +472,7 @@ class MarkdownNoteDetail extends React.Component {
this.handleSwitchMode(e)} editorType={editorType} /> + this.handleStarButtonClick(e)} isActive={note.isStarred} @@ -473,7 +485,7 @@ class MarkdownNoteDetail extends React.Component { onFocus={(e) => this.handleFocus(e)} onMouseDown={(e) => this.handleLockButtonMouseDown(e)} > - + {this.state.isLocked ? Unlock : Lock} @@ -500,7 +512,7 @@ class MarkdownNoteDetail extends React.Component { exportAsTxt={this.exportAsTxt} exportAsHtml={this.exportAsHtml} exportAsPdf={this.exportAsPdf} - wordCount={note.content.split(' ').length} + wordCount={note.content.trim().split(/\s+/g).length} letterCount={note.content.replace(/\r?\n/g, '').length} type={note.type} print={this.print} diff --git a/browser/main/Detail/MarkdownNoteDetail.styl b/browser/main/Detail/MarkdownNoteDetail.styl index 819bef2e..a24e9881 100644 --- a/browser/main/Detail/MarkdownNoteDetail.styl +++ b/browser/main/Detail/MarkdownNoteDetail.styl @@ -81,11 +81,4 @@ body[data-theme="dracula"] .root border-left 1px solid $ui-dracula-borderColor background-color $ui-dracula-noteDetail-backgroundColor - -div - > button, div - -webkit-user-drag none - user-select none - > img, span - -webkit-user-drag none - user-select none + diff --git a/browser/main/Detail/NoteDetailInfo.styl b/browser/main/Detail/NoteDetailInfo.styl index 1ca46516..21670a1b 100644 --- a/browser/main/Detail/NoteDetailInfo.styl +++ b/browser/main/Detail/NoteDetailInfo.styl @@ -107,4 +107,12 @@ body[data-theme="monokai"] body[data-theme="dracula"] .info border-color $ui-dracula-borderColor - background-color $ui-dracula-noteDetail-backgroundColor \ No newline at end of file + background-color $ui-dracula-noteDetail-backgroundColor + +.info > div + > button + -webkit-user-drag none + user-select none + > img, span + -webkit-user-drag none + user-select none \ No newline at end of file diff --git a/browser/main/Detail/PermanentDeleteButton.js b/browser/main/Detail/PermanentDeleteButton.js index fa00ef17..7c27ede1 100644 --- a/browser/main/Detail/PermanentDeleteButton.js +++ b/browser/main/Detail/PermanentDeleteButton.js @@ -10,7 +10,7 @@ const PermanentDeleteButton = ({ ) diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 7503addb..ec9a1d0b 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -518,6 +518,19 @@ class SnippetNoteDetail extends React.Component { ]) } + handleWrapLineButtonClick (e) { + context.popup([ + { + label: 'on', + click: (e) => this.handleWrapLineItemClick(e, true) + }, + { + label: 'off', + click: (e) => this.handleWrapLineItemClick(e, false) + } + ]) + } + handleIndentSizeItemClick (e, indentSize) { const { config, dispatch } = this.props const editor = Object.assign({}, config.editor, { @@ -550,6 +563,22 @@ class SnippetNoteDetail extends React.Component { }) } + handleWrapLineItemClick (e, lineWrapping) { + const { config, dispatch } = this.props + const editor = Object.assign({}, config.editor, { + lineWrapping + }) + ConfigManager.set({ + editor + }) + dispatch({ + type: 'SET_CONFIG', + config: { + editor + } + }) + } + focus () { this.refs.description.focus() } @@ -670,7 +699,7 @@ class SnippetNoteDetail extends React.Component { } render () { - const { data, config, location } = this.props + const { data, dispatch, config, location } = this.props const { note } = this.state const storageKey = note.storage @@ -720,6 +749,7 @@ class SnippetNoteDetail extends React.Component { mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)} value={snippet.content} linesHighlighted={snippet.linesHighlighted} + lineWrapping={config.editor.lineWrapping} theme={config.editor.theme} fontFamily={config.editor.fontFamily} fontSize={editorFontSize} @@ -778,7 +808,7 @@ class SnippetNoteDetail extends React.Component { const detailTopBar =
-
+
this.handleChange(e)} coloredTags={config.coloredTags} /> @@ -899,6 +930,12 @@ class SnippetNoteDetail extends React.Component { size: {config.editor.indentSize}  +
(
-
onClick('SPLIT')}> - +
onClick('SPLIT')}> +
-
onClick('EDITOR_PREVIEW')}> - +
onClick('EDITOR_PREVIEW')}> +
{i18n.__('Toggle Mode')}
@@ -20,7 +20,7 @@ const ToggleModeButton = ({ ToggleModeButton.propTypes = { onClick: PropTypes.func.isRequired, - editorType: PropTypes.string.Required + editorType: PropTypes.string.isRequired } export default CSSModules(ToggleModeButton, styles) diff --git a/browser/main/Detail/ToggleModeButton.styl b/browser/main/Detail/ToggleModeButton.styl index 2b47b932..39d30973 100644 --- a/browser/main/Detail/ToggleModeButton.styl +++ b/browser/main/Detail/ToggleModeButton.styl @@ -75,3 +75,10 @@ body[data-theme="dracula"] .active background-color #bd93f9 box-shadow 2px 0px 7px #222222 + +.control-toggleModeButton + -webkit-user-drag none + user-select none + > div img + -webkit-user-drag none + user-select none diff --git a/browser/main/Detail/TrashButton.js b/browser/main/Detail/TrashButton.js index d26be66e..8ca27ce9 100644 --- a/browser/main/Detail/TrashButton.js +++ b/browser/main/Detail/TrashButton.js @@ -10,7 +10,7 @@ const TrashButton = ({ ) diff --git a/browser/main/Detail/index.js b/browser/main/Detail/index.js index 0ed3dd54..95b9d73d 100644 --- a/browser/main/Detail/index.js +++ b/browser/main/Detail/index.js @@ -50,16 +50,14 @@ class Detail extends React.Component { const searchStr = params.searchword displayedNotes = searchStr === undefined || searchStr === '' ? allNotes : searchFromNotes(allNotes, searchStr) - } - - if (location.pathname.match(/\/tags/)) { + } 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)) ) } - if (location.pathname.match(/\/trashed/)) { + if (location.pathname.match(/^\/trashed/)) { displayedNotes = trashedNotes } else { displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key) diff --git a/browser/main/DevTools/index.js b/browser/main/DevTools/index.js index 93d666a2..d39d5fbb 100644 --- a/browser/main/DevTools/index.js +++ b/browser/main/DevTools/index.js @@ -1,8 +1,8 @@ /* eslint-disable no-undef */ -if (process.env.NODE_ENV === 'production') { - // eslint-disable-next-line global-require - module.exports = require('./index.prod').default -} else { +if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line global-require module.exports = require('./index.dev').default +} else { + // eslint-disable-next-line global-require + module.exports = require('./index.prod').default } diff --git a/browser/main/Main.js b/browser/main/Main.js index 30bf8e8a..e277c421 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -102,7 +102,7 @@ class Main extends React.Component { { name: 'example.js', mode: 'javascript', - content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)", + content: "var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)", linesHighlighted: [] } ] @@ -169,6 +169,7 @@ class Main extends React.Component { } }) + // eslint-disable-next-line no-undef delete CodeMirror.keyMap.emacs['Ctrl-V'] eventEmitter.on('editor:fullscreen', this.toggleFullScreen) diff --git a/browser/main/NewNoteButton/index.js b/browser/main/NewNoteButton/index.js index 115d9530..27e2baa5 100644 --- a/browser/main/NewNoteButton/index.js +++ b/browser/main/NewNoteButton/index.js @@ -90,7 +90,7 @@ class NewNoteButton extends React.Component {
@@ -1155,6 +1163,7 @@ class NoteList extends React.Component { tabIndex='-1' onKeyDown={(e) => this.handleNoteListKeyDown(e)} onKeyUp={this.handleNoteListKeyUp} + onBlur={this.handleNoteListBlur} > {noteList}
diff --git a/browser/main/SideNav/PreferenceButton.js b/browser/main/SideNav/PreferenceButton.js index 187171f4..187bc41a 100644 --- a/browser/main/SideNav/PreferenceButton.js +++ b/browser/main/SideNav/PreferenceButton.js @@ -8,7 +8,7 @@ const PreferenceButton = ({ onClick }) => ( ) diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index 74881b9e..5cd4a491 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -362,14 +362,14 @@ class StorageItem extends React.Component { }
{this.state.isOpen && -
+
{folderList}
} diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index fc665052..3167f487 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -22,9 +22,10 @@ import context from 'browser/lib/context' import { remote } from 'electron' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import ColorPicker from 'browser/components/ColorPicker' +import { every, sortBy } from 'lodash' function matchActiveTags (tags, activeTags) { - return _.every(activeTags, v => tags.indexOf(v) >= 0) + return every(activeTags, v => tags.indexOf(v) >= 0) } class SideNav extends React.Component { @@ -271,6 +272,7 @@ class SideNav extends React.Component {
{this.tagListComponent(data)}
+
) } @@ -283,7 +285,7 @@ class SideNav extends React.Component { const { colorPicker } = this.state const activeTags = this.getActiveTags(location.pathname) const relatedTags = this.getRelatedTags(activeTags, data.noteMap) - let tagList = _.sortBy(data.tagNoteMap.map( + let tagList = sortBy(data.tagNoteMap.map( (tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) }) ).filter( tag => tag.size > 0 @@ -296,7 +298,7 @@ class SideNav extends React.Component { }) } 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( @@ -440,7 +442,7 @@ class SideNav extends React.Component { const style = {} if (!isFolded) style.width = this.props.width - const isTagActive = location.pathname.match(/tag/) + const isTagActive = /tag/.test(location.pathname) return (
{ this.handleOnSearchFocus() } this.codeInitHandler = this.handleCodeInit.bind(this) - this.updateKeyword = this.updateKeyword.bind(this) + this.handleKeyDown = this.handleKeyDown.bind(this) + this.handleSearchFocus = this.handleSearchFocus.bind(this) + this.handleSearchBlur = this.handleSearchBlur.bind(this) + this.handleSearchChange = this.handleSearchChange.bind(this) this.handleSearchClearButton = this.handleSearchClearButton.bind(this) - this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, { + this.debouncedUpdateKeyword = debounce((keyword) => { + dispatch(push(`/searched/${encodeURIComponent(keyword)}`)) + this.setState({ + search: keyword + }) + ee.emit('top:search', keyword) + }, 1000 / 60, { maxWait: 1000 / 8 }) } @@ -63,14 +71,14 @@ class TopBar extends React.Component { this.refs.search.childNodes[0].blur dispatch(push('/searched')) e.preventDefault() + this.debouncedUpdateKeyword('') } handleKeyDown (e) { - // reset states - this.setState({ - isAlphabet: false, - isIME: false - }) + // Re-apply search field on ENTER key + if (e.keyCode === 13) { + this.debouncedUpdateKeyword(e.target.value) + } // Clear search on ESC if (e.keyCode === 27) { @@ -88,52 +96,11 @@ class TopBar extends React.Component { ee.emit('list:prior') e.preventDefault() } - - // When the key is an alphabet, del, enter or ctr - if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) { - this.setState({ - isAlphabet: true - }) - // When the key is an IME input (Japanese, Chinese) - } else if (e.keyCode === 229) { - this.setState({ - isIME: true - }) - } - } - - handleKeyUp (e) { - // reset states - this.setState({ - isConfirmTranslation: false - }) - - // When the key is translation confirmation (Enter, Space) - if (this.state.isIME && (e.keyCode === 32 || e.keyCode === 13)) { - this.setState({ - isConfirmTranslation: true - }) - const keyword = this.refs.searchInput.value - this.updateKeyword(keyword) - } } handleSearchChange (e) { - if (this.state.isAlphabet || this.state.isConfirmTranslation) { - const keyword = this.refs.searchInput.value - this.updateKeyword(keyword) - } else { - e.preventDefault() - } - } - - updateKeyword (keyword) { - const { dispatch } = this.props - dispatch(push(`/searched/${encodeURIComponent(keyword)}`)) - this.setState({ - search: keyword - }) - ee.emit('top:search', keyword) + const keyword = e.target.value + this.debouncedUpdateKeyword(keyword) } handleSearchFocus (e) { @@ -141,6 +108,7 @@ class TopBar extends React.Component { isSearching: true }) } + handleSearchBlur (e) { e.stopPropagation() @@ -170,7 +138,7 @@ class TopBar extends React.Component { } handleCodeInit () { - ee.emit('top:search', this.refs.searchInput.value) + ee.emit('top:search', this.refs.searchInput.value || '') } render () { @@ -183,24 +151,23 @@ class TopBar extends React.Component {
this.handleSearchFocus(e)} - onBlur={(e) => this.handleSearchBlur(e)} + onFocus={this.handleSearchFocus} + onBlur={this.handleSearchBlur} tabIndex='-1' ref='search' > - this.handleSearchChange(e)} - onKeyDown={(e) => this.handleKeyDown(e)} - onKeyUp={(e) => this.handleKeyUp(e)} + onInputChange={this.handleSearchChange} + onKeyDown={this.handleKeyDown} placeholder={i18n.__('Search')} type='text' className='searchInput' /> {this.state.search !== '' && +
{this.state.errormessage}
+
+
+ ) + } +} + +CreateMarkdownFromURLModal.propTypes = { + storage: PropTypes.string, + folder: PropTypes.string, + dispatch: PropTypes.func, + location: PropTypes.shape({ + pathname: PropTypes.string + }) +} + +export default CSSModules(CreateMarkdownFromURLModal, styles) diff --git a/browser/main/modals/CreateMarkdownFromURLModal.styl b/browser/main/modals/CreateMarkdownFromURLModal.styl new file mode 100644 index 00000000..5e59e465 --- /dev/null +++ b/browser/main/modals/CreateMarkdownFromURLModal.styl @@ -0,0 +1,160 @@ +.root + modal() + width 500px + height 270px + overflow hidden + position relative + +.header + height 80px + margin-bottom 10px + margin-top 20px + font-size 18px + line-height 50px + background-color $ui-backgroundColor + color $ui-text-color + +.title + font-size 36px + font-weight 600 + +.control-folder-label + text-align left + font-size 14px + color $ui-text-color + +.control-folder-input + display block + height 40px + width 490px + padding 0 5px + margin 10px 0 + border 1px solid $ui-input--create-folder-modal + border-radius 2px + background-color transparent + outline none + vertical-align middle + font-size 16px + &:disabled + background-color $ui-input--disabled-backgroundColor + &:focus, &:active + border-color $ui-active-color + +.control-confirmButton + display block + height 35px + width 140px + border none + border-radius 2px + padding 0 25px + margin 20px auto + font-size 14px + colorPrimaryButton() + +body[data-theme="dark"] + .root + modalDark() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-borderColor + color $ui-dark-text-color + + .control-folder-label + color $ui-dark-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorDarkPrimaryButton() + +body[data-theme="solarized-dark"] + .root + modalSolarizedDark() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-borderColor + color $ui-solarized-dark-text-color + + .control-folder-label + color $ui-solarized-dark-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorSolarizedDarkPrimaryButton() + +.error + text-align center + color #F44336 + +body[data-theme="monokai"] + .root + modalMonokai() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-borderColor + color $ui-monokai-text-color + + .control-folder-label + color $ui-monokai-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorMonokaiPrimaryButton() + +body[data-theme="dracula"] + .root + modalDracula() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dracula-borderColor + color $ui-dracula-text-color + + .control-folder-label + color $ui-dracula-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorDraculaPrimaryButton() diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index a17a36cd..476fa252 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -3,6 +3,8 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './NewNoteModal.styl' import ModalEscButton from 'browser/components/ModalEscButton' import i18n from 'browser/lib/i18n' +import { openModal } from 'browser/main/lib/modal' +import CreateMarkdownFromURLModal from '../modals/CreateMarkdownFromURLModal' import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' import queryString from 'query-string' @@ -21,6 +23,18 @@ class NewNoteModal extends React.Component { this.props.close() } + handleCreateMarkdownFromUrlClick (e) { + this.props.close() + + const { storage, folder, dispatch, location } = this.props + openModal(CreateMarkdownFromURLModal, { + storage: storage, + folder: folder, + dispatch, + location + }) + } + handleMarkdownNoteButtonClick (e) { const { storage, folder, dispatch, location, config } = this.props const params = location.search !== '' && queryString.parse(location.search) @@ -115,10 +129,8 @@ class NewNoteModal extends React.Component {
-
- {i18n.__('Tab to switch format')} -
- +
{i18n.__('Tab to switch format')}
+
this.handleCreateMarkdownFromUrlClick(e)}>Or, create a new markdown note from a URL
) } diff --git a/browser/main/modals/NewNoteModal.styl b/browser/main/modals/NewNoteModal.styl index c82b9376..ff0052bd 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -19,6 +19,7 @@ .control padding 25px 0px text-align center + display: flex .control-button width 240px @@ -47,6 +48,12 @@ text-align center margin-bottom 25px +.from-url + color $ui-inactive-text-color + text-align center + margin-bottom 25px + cursor pointer + body[data-theme="dark"] .root modalDark() @@ -61,7 +68,7 @@ body[data-theme="dark"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-inactive-text-color body[data-theme="solarized-dark"] @@ -78,7 +85,7 @@ body[data-theme="solarized-dark"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-solarized-dark-text-color body[data-theme="monokai"] @@ -95,7 +102,7 @@ body[data-theme="monokai"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-monokai-text-color body[data-theme="dracula"] diff --git a/browser/main/modals/PreferencesModal/FolderItem.js b/browser/main/modals/PreferencesModal/FolderItem.js index e6bd1e37..648db4e6 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.js +++ b/browser/main/modals/PreferencesModal/FolderItem.js @@ -225,7 +225,7 @@ class FolderItem extends React.Component {
- {folder.name} + {folder.name} ({folder.key})
@@ -288,10 +288,10 @@ class Handle extends React.Component { class SortableFolderItemComponent extends React.Component { render () { - const StyledHandle = CSSModules(Handle, this.props.styles) + const StyledHandle = CSSModules(Handle, styles) const DragHandle = SortableHandle(StyledHandle) - const StyledFolderItem = CSSModules(FolderItem, this.props.styles) + const StyledFolderItem = CSSModules(FolderItem, styles) return (
diff --git a/browser/main/modals/PreferencesModal/FolderList.js b/browser/main/modals/PreferencesModal/FolderList.js index 02f5cee9..674026c5 100644 --- a/browser/main/modals/PreferencesModal/FolderList.js +++ b/browser/main/modals/PreferencesModal/FolderList.js @@ -22,7 +22,7 @@ class FolderList extends React.Component { }) return ( -
+
{folderList.length > 0 ? folderList :
{i18n.__('No Folders')}
diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 713f6a65..9c4f5655 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -76,13 +76,16 @@ class HotkeyTab extends React.Component { handleHotkeyChange (e) { const { config } = this.state - config.hotkey = { + config.hotkey = Object.assign({}, config.hotkey, { toggleMain: this.refs.toggleMain.value, toggleMode: this.refs.toggleMode.value, deleteNote: this.refs.deleteNote.value, pasteSmartly: this.refs.pasteSmartly.value, - toggleMenuBar: this.refs.toggleMenuBar.value - } + prettifyMarkdown: this.refs.prettifyMarkdown.value, + toggleMenuBar: this.refs.toggleMenuBar.value, + insertDate: this.refs.insertDate.value, + insertDateTime: this.refs.insertDateTime.value + }) this.setState({ config }) @@ -173,6 +176,38 @@ class HotkeyTab extends React.Component { />
+
+
{i18n.__('Prettify Markdown')}
+
+ this.handleHotkeyChange(e)} + ref='prettifyMarkdown' + value={config.hotkey.prettifyMarkdown} + type='text' /> +
+
+
+
{i18n.__('Insert Current Date')}
+
+ +
+
+
+
{i18n.__('Insert Current Date and Time')}
+
+ +
+
+
{i18n.__('Attachment storage')}
+

+ Unused attachments size: {humanFileSize(totalUnusedAttachmentsSize)} ({totalUnusedAttachments} items) +

+

+ In use attachments size: {humanFileSize(totalInuseAttachmentsSize)} ({totalInuseAttachments} items) +

+

+ Total attachments size: {humanFileSize(totalAttachmentsSize)} ({totalAttachments} items) +

+
) } diff --git a/browser/main/modals/PreferencesModal/StoragesTab.styl b/browser/main/modals/PreferencesModal/StoragesTab.styl index b63cc85e..fbfa89e6 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.styl +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -33,6 +33,17 @@ colorDefaultButton() font-size $tab--button-font-size border-radius 2px +.list-attachment-label + margin-bottom 10px + color $ui-text-color +.list-attachement-clear-button + height 30px + border none + border-top-right-radius 2px + border-bottom-right-radius 2px + colorPrimaryButton() + vertical-align middle + padding 0 20px .addStorage margin-bottom 15px @@ -154,8 +165,8 @@ body[data-theme="dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-dark-borderColor - - + .list-attachement-clear-button + colorDarkPrimaryButton() body[data-theme="solarized-dark"] .root @@ -194,6 +205,8 @@ body[data-theme="solarized-dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-solarized-dark-borderColor + .list-attachement-clear-button + colorSolarizedDarkPrimaryButton() body[data-theme="monokai"] .root @@ -232,6 +245,8 @@ body[data-theme="monokai"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-monokai-borderColor + .list-attachement-clear-button + colorMonokaiPrimaryButton() body[data-theme="dracula"] .root @@ -269,4 +284,6 @@ body[data-theme="dracula"] colorDarkPrimaryButton() .addStorage-body-control-cancelButton colorDarkDefaultButton() - border-color $ui-dracula-borderColor \ No newline at end of file + border-color $ui-dracula-borderColor + .list-attachement-clear-button + colorDraculaPrimaryButton() \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index f74dbda5..329dbfa4 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -31,8 +31,12 @@ class UiTab extends React.Component { CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript') CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css') CodeMirror.autoLoadMode(this.customMarkdownLintConfigCM.getCodeMirror(), 'javascript') + CodeMirror.autoLoadMode(this.prettierConfigCM.getCodeMirror(), 'javascript') + // Set CM editor Sizes this.customCSSCM.getCodeMirror().setSize('400px', '400px') + this.prettierConfigCM.getCodeMirror().setSize('400px', '400px') this.customMarkdownLintConfigCM.getCodeMirror().setSize('400px', '200px') + this.handleSettingDone = () => { this.setState({UiAlert: { type: 'success', @@ -91,6 +95,7 @@ class UiTab extends React.Component { enableRulers: this.refs.enableEditorRulers.value === 'true', rulers: this.refs.editorRulers.value.replace(/[^0-9,]/g, '').split(','), displayLineNumbers: this.refs.editorDisplayLineNumbers.checked, + lineWrapping: this.refs.editorLineWrapping.checked, switchPreview: this.refs.editorSwitchPreview.value, keyMap: this.refs.editorKeyMap.value, snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value, @@ -105,7 +110,9 @@ class UiTab extends React.Component { spellcheck: this.refs.spellcheck.checked, enableSmartPaste: this.refs.enableSmartPaste.checked, enableMarkdownLint: this.refs.enableMarkdownLint.checked, - customMarkdownLintConfig: this.customMarkdownLintConfigCM.getCodeMirror().getValue() + customMarkdownLintConfig: this.customMarkdownLintConfigCM.getCodeMirror().getValue(), + prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(), + deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked }, preview: { fontSize: this.refs.previewFontSize.value, @@ -123,6 +130,7 @@ class UiTab extends React.Component { breaks: this.refs.previewBreaks.checked, smartArrows: this.refs.previewSmartArrows.checked, sanitize: this.refs.previewSanitize.value, + mermaidHTMLLabel: this.refs.previewMermaidHTMLLabel.checked, allowCustomCSS: this.refs.previewAllowCustomCSS.checked, lineThroughCheckbox: this.refs.lineThroughCheckbox.checked, customCSS: this.customCSSCM.getCodeMirror().getValue() @@ -135,7 +143,7 @@ class UiTab extends React.Component { const theme = consts.THEMES.find(theme => theme.name === newCodemirrorTheme) if (theme) { - checkHighLight.setAttribute('href', `../${theme.path}`) + checkHighLight.setAttribute('href', theme.path) } } @@ -545,6 +553,17 @@ class UiTab extends React.Component {
+
+ +
+
+
+ +
@@ -800,6 +829,16 @@ class UiTab extends React.Component {
+
+ +
{i18n.__('LaTeX Inline Open Delimiter')} @@ -883,7 +922,6 @@ class UiTab extends React.Component { onChange={e => this.handleUIChange(e)} ref={e => (this.customCSSCM = e)} value={config.preview.customCSS} - defaultValue={'/* Drop Your Custom CSS Code Here */\n'} options={{ lineNumbers: true, mode: 'css', @@ -892,7 +930,27 @@ class UiTab extends React.Component {
- +
+
+ {i18n.__('Prettier Config')} +
+
+
+ this.handleUIChange(e)} + ref={e => (this.prettierConfigCM = e)} + value={config.editor.prettierConfig} + options={{ + lineNumbers: true, + mode: 'application/json', + lint: true, + theme: codemirrorTheme + }} /> +
+
+