diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..a742a59e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,41 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "type": "node", + "request": "launch", + "name": "BoostNote Main", + "protocol": "inspector", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "runtimeArgs": [ + "--remote-debugging-port=9223", + "--hot", + "${workspaceFolder}/index.js" + ], + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modeules/.bin/electron.cmd" + } + }, + { + "type": "chrome", + "request": "attach", + "name": "BoostNote Renderer", + "port": 9223, + "webRoot": "${workspaceFolder}", + "sourceMapPathOverrides": { + "webpack:///./~/*": "${webRoot}/node_modules/*", + "webpack:///*": "${webRoot}/*" + } + } + ], + "compounds": [ + { + "name": "BostNote All", + "configurations": ["BoostNote Main", "BoostNote Renderer"] + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..c6664225 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,27 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Build Boostnote", + "group": "build", + "type": "npm", + "script": "watch", + "isBackground": true, + "presentation": { + "reveal": "always", + }, + "problemMatcher": { + "pattern":[ + { + "regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$", + "file": 1, + "location": 2, + "message": 3 + } + ] + } + } + ] +} \ No newline at end of file diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 1f5ada57..5554c4b8 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,15 +1,25 @@ # Current behavior # Expected behavior + + # Steps to reproduce + + 1. 2. 3. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..58df576a --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ + +## Description + + +## Issue fixed + + + +## Type of changes + +- :white_circle: Bug fix (Change that fixed an issue) +- :white_circle: Breaking change (Change that can cause existing functionality to change) +- :white_circle: Improvement (Change that improves the code. Maybe performance or development improvement) +- :white_circle: Feature (Change that adds new functionality) +- :white_circle: Documentation change (Change that modifies documentation. Maybe typo fixes) + +## Checklist: + +- :white_circle: My code follows [the project code style](docs/code_style.md) +- :white_circle: I have written test for my code and it has been tested +- :white_circle: All existing tests have been passed +- :white_circle: I have attached a screenshot/video to visualize my change if possible diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index c36a50c1..7719ed90 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -11,15 +11,24 @@ import eventEmitter from 'browser/main/lib/eventEmitter' import iconv from 'iconv-lite' import crypto from 'crypto' import consts from 'browser/lib/consts' +import styles from '../components/CodeEditor.styl' import fs from 'fs' -const { ipcRenderer } = require('electron') +const { ipcRenderer, remote, clipboard } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' +const spellcheck = require('browser/lib/spellcheck') +const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') +import TurndownService from 'turndown' +import { gfm } from 'turndown-plugin-gfm' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' const buildCMRulers = (rulers, enableRulers) => (enableRulers ? rulers.map(ruler => ({ column: ruler })) : []) +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) { super(props) @@ -28,7 +37,7 @@ export default class CodeEditor extends React.Component { leading: false, trailing: true }) - this.changeHandler = e => this.handleChange(e) + this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject) this.focusHandler = () => { ipcRenderer.send('editor:focused', true) } @@ -51,15 +60,32 @@ export default class CodeEditor extends React.Component { noteKey ) } - this.pasteHandler = (editor, e) => this.handlePaste(editor, e) + this.pasteHandler = (editor, e) => { + e.preventDefault() + + this.handlePaste(editor, false) + } this.loadStyleHandler = e => { this.editor.refresh() } this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchState = null + this.scrollToLineHandeler = this.scrollToLine.bind(this) this.formatTable = () => this.handleFormatTable() + + if (props.switchPreview !== 'RIGHTCLICK') { + this.contextMenuHandler = function (editor, event) { + const menu = buildEditorContextMenu(editor, event) + if (menu != null) { + setTimeout(() => menu.popup(remote.getCurrentWindow()), 30) + } + } + } + this.editorActivityHandler = () => this.handleEditorActivity() + + this.turndownService = new TurndownService() } handleSearch (msg) { @@ -106,42 +132,10 @@ export default class CodeEditor extends React.Component { } } - updateTableEditorState () { - const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) - if (active) { - if (this.extraKeysMode !== 'editor') { - this.extraKeysMode = 'editor' - this.editor.setOption('extraKeys', this.editorKeyMap) - } - } else { - if (this.extraKeysMode !== 'default') { - this.extraKeysMode = 'default' - this.editor.setOption('extraKeys', this.defaultKeyMap) - this.tableEditor.resetSmartCursor() - } - } - } - - componentDidMount () { - const { rulers, enableRulers } = this.props + updateDefaultKeyMap () { + const { hotkey } = this.props const expandSnippet = this.expandSnippet.bind(this) - const defaultSnippet = [ - { - id: crypto.randomBytes(16).toString('hex'), - name: 'Dummy text', - prefix: ['lorem', 'ipsum'], - content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' - } - ] - if (!fs.existsSync(consts.SNIPPET_FILE)) { - fs.writeFileSync( - consts.SNIPPET_FILE, - JSON.stringify(defaultSnippet, null, 4), - 'utf8' - ) - } - this.defaultKeyMap = CodeMirror.normalizeKeyMap({ Tab: function (cm) { const cursor = cm.getCursor() @@ -192,8 +186,50 @@ export default class CodeEditor extends React.Component { document.execCommand('copy') } return CodeMirror.Pass + }, + [translateHotkey(hotkey.pasteSmartly)]: cm => { + this.handlePaste(cm, true) } }) + } + + updateTableEditorState () { + const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) + if (active) { + if (this.extraKeysMode !== 'editor') { + this.extraKeysMode = 'editor' + this.editor.setOption('extraKeys', this.editorKeyMap) + } + } else { + if (this.extraKeysMode !== 'default') { + this.extraKeysMode = 'default' + this.editor.setOption('extraKeys', this.defaultKeyMap) + this.tableEditor.resetSmartCursor() + } + } + } + + componentDidMount () { + const { rulers, enableRulers } = this.props + eventEmitter.on('line:jump', this.scrollToLineHandeler) + + const defaultSnippet = [ + { + id: crypto.randomBytes(16).toString('hex'), + name: 'Dummy text', + prefix: ['lorem', 'ipsum'], + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + } + ] + if (!fs.existsSync(consts.SNIPPET_FILE)) { + fs.writeFileSync( + consts.SNIPPET_FILE, + JSON.stringify(defaultSnippet, null, 4), + 'utf8' + ) + } + + this.updateDefaultKeyMap() this.value = this.props.value this.editor = CodeMirror(this.refs.root, { @@ -226,6 +262,9 @@ export default class CodeEditor extends React.Component { this.editor.on('blur', this.blurHandler) this.editor.on('change', this.changeHandler) this.editor.on('paste', this.pasteHandler) + if (this.props.switchPreview !== 'RIGHTCLICK') { + this.editor.on('contextmenu', this.contextMenuHandler) + } eventEmitter.on('top:search', this.searchHandler) eventEmitter.emit('code:init') @@ -242,6 +281,10 @@ 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'}) + } + eventEmitter.on('code:format-table', this.formatTable) this.tableEditorOptions = options({ @@ -311,22 +354,28 @@ export default class CodeEditor extends React.Component { const snippetLines = snippets[i].content.split('\n') let cursorLineNumber = 0 let cursorLinePosition = 0 + + let cursorIndex for (let j = 0; j < snippetLines.length; j++) { - const cursorIndex = snippetLines[j].indexOf(templateCursorString) + cursorIndex = snippetLines[j].indexOf(templateCursorString) + if (cursorIndex !== -1) { cursorLineNumber = j cursorLinePosition = cursorIndex - cm.replaceRange( - snippets[i].content.replace(templateCursorString, ''), - wordBeforeCursor.range.from, - wordBeforeCursor.range.to - ) - cm.setCursor({ - line: cursor.line + cursorLineNumber, - ch: cursorLinePosition - }) + + break } } + + cm.replaceRange( + snippets[i].content.replace(templateCursorString, ''), + wordBeforeCursor.range.from, + wordBeforeCursor.range.to + ) + cm.setCursor({ + line: cursor.line + cursorLineNumber, + ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length + }) } else { cm.replaceRange( snippets[i].content, @@ -383,9 +432,11 @@ export default class CodeEditor extends React.Component { this.editor.off('paste', this.pasteHandler) eventEmitter.off('top:search', this.searchHandler) this.editor.off('scroll', this.scrollHandler) + this.editor.off('contextmenu', this.contextMenuHandler) const editorTheme = document.getElementById('editorTheme') editorTheme.removeEventListener('load', this.loadStyleHandler) + spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED) eventEmitter.off('code:format-table', this.formatTable) } @@ -445,6 +496,14 @@ export default class CodeEditor extends React.Component { this.editor.setOption('extraKeys', this.defaultKeyMap) } + if (prevProps.hotkey !== this.props.hotkey) { + this.updateDefaultKeyMap() + + if (this.extraKeysMode === 'default') { + this.editor.setOption('extraKeys', this.defaultKeyMap) + } + } + if (this.state.clientWidth !== this.refs.root.clientWidth) { this.setState({ clientWidth: this.refs.root.clientWidth @@ -453,6 +512,16 @@ export default class CodeEditor extends React.Component { needRefresh = true } + if (prevProps.spellCheck !== this.props.spellCheck) { + if (this.props.spellCheck === false) { + spellcheck.setLanguage(this.editor, spellcheck.SPELLCHECK_DISABLED) + let elem = document.getElementById('editor-bottom-panel') + elem.parentNode.removeChild(elem) + } else { + this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) + } + } + if (needRefresh) { this.editor.refresh() } @@ -466,16 +535,23 @@ export default class CodeEditor extends React.Component { CodeMirror.autoLoadMode(this.editor, syntax.mode) } - handleChange (e) { - this.value = this.editor.getValue() + handleChange (editor, changeObject) { + spellcheck.handleChange(editor, changeObject) + this.value = editor.getValue() if (this.props.onChange) { - this.props.onChange(e) + this.props.onChange(editor) } } moveCursorTo (row, col) {} - scrollToLine (num) {} + scrollToLine (event, num) { + const cursor = { + line: num, + ch: 1 + } + this.editor.setCursor(cursor) + } focus () { this.editor.focus() @@ -516,15 +592,14 @@ export default class CodeEditor extends React.Component { this.editor.replaceSelection(imageMd) } - handlePaste (editor, e) { - const clipboardData = e.clipboardData - const { storageKey, noteKey } = this.props - const dataTransferItem = clipboardData.items[0] - const pastedTxt = clipboardData.getData('text') + handlePaste (editor, forceSmartPaste) { + const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props + const isURL = str => { const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ return matcher.test(str) } + const isInLinkTag = editor => { const startCursor = editor.getCursor('start') const prevChar = editor.getRange( @@ -538,27 +613,74 @@ export default class CodeEditor extends React.Component { ) return prevChar === '](' && nextChar === ')' } - if (dataTransferItem.type.match('image')) { - attachmentManagement.handlePastImageEvent( - this, - storageKey, - noteKey, - dataTransferItem - ) - } else if ( - this.props.fetchUrlTitle && - isURL(pastedTxt) && - !isInLinkTag(editor) - ) { - this.handlePasteUrl(e, editor, pastedTxt) + + const isInFencedCodeBlock = editor => { + const cursor = editor.getCursor() + + let token = editor.getTokenAt(cursor) + if (token.state.fencedState) { + return true + } + + let line = line = cursor.line - 1 + while (line >= 0) { + token = editor.getTokenAt({ + ch: 3, + line + }) + + if (token.start === token.end) { + --line + } else if (token.type === 'comment') { + if (line > 0) { + token = editor.getTokenAt({ + ch: 3, + line: line - 1 + }) + + return token.type !== 'comment' + } else { + return true + } + } else { + return false + } + } + + return false } - if (attachmentManagement.isAttachmentLink(pastedTxt)) { + + const pastedTxt = clipboard.readText() + + if (isInFencedCodeBlock(editor)) { + this.handlePasteText(editor, pastedTxt) + } else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { + this.handlePasteUrl(editor, pastedTxt) + } else if (enableSmartPaste || forceSmartPaste) { + const image = clipboard.readImage() + if (!image.isEmpty()) { + attachmentManagement.handlePastNativeImage( + this, + storageKey, + noteKey, + image + ) + } else { + const pastedHtml = clipboard.readHTML() + if (pastedHtml.length > 0) { + this.handlePasteHtml(editor, pastedHtml) + } else { + this.handlePasteText(editor, pastedTxt) + } + } + } else if (attachmentManagement.isAttachmentLink(pastedTxt)) { attachmentManagement .handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt) .then(modifiedText => { this.editor.replaceSelection(modifiedText) }) - e.preventDefault() + } else { + this.handlePasteText(editor, pastedTxt) } } @@ -568,8 +690,7 @@ export default class CodeEditor extends React.Component { } } - handlePasteUrl (e, editor, pastedTxt) { - e.preventDefault() + handlePasteUrl (editor, pastedTxt) { const taggedUrl = `<${pastedTxt}>` editor.replaceSelection(taggedUrl) @@ -608,6 +729,15 @@ export default class CodeEditor extends React.Component { }) } + handlePasteHtml (editor, pastedHtml) { + const markdown = this.turndownService.turndown(pastedHtml) + editor.replaceSelection(markdown) + } + + handlePasteText (editor, pastedTxt) { + editor.replaceSelection(pastedTxt) + } + mapNormalResponse (response, pastedTxt) { return this.decodeResponse(response).then(body => { return new Promise((resolve, reject) => { @@ -690,6 +820,25 @@ export default class CodeEditor extends React.Component { /> ) } + + 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)) + const options = spellcheck.getAvailableDictionaries() + for (const op of options) { + const option = document.createElement('option') + option.value = op.value + option.innerHTML = op.label + dropdown.appendChild(option) + } + panel.appendChild(dropdown) + return panel + } } CodeEditor.propTypes = { @@ -700,7 +849,8 @@ CodeEditor.propTypes = { className: PropTypes.string, onBlur: PropTypes.func, onChange: PropTypes.func, - readOnly: PropTypes.bool + readOnly: PropTypes.bool, + spellCheck: PropTypes.bool } CodeEditor.defaultProps = { @@ -710,5 +860,6 @@ CodeEditor.defaultProps = { fontSize: 14, fontFamily: 'Monaco, Consolas', indentSize: 4, - indentType: 'space' + indentType: 'space', + spellCheck: false } diff --git a/browser/components/CodeEditor.styl b/browser/components/CodeEditor.styl new file mode 100644 index 00000000..7a254935 --- /dev/null +++ b/browser/components/CodeEditor.styl @@ -0,0 +1,6 @@ +.codeEditor-typo + text-decoration underline wavy red + +.spellcheck-select + border: none + text-decoration underline wavy red diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 4c195797..d3270c18 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -6,6 +6,7 @@ import CodeEditor from 'browser/components/CodeEditor' import MarkdownPreview from 'browser/components/MarkdownPreview' import eventEmitter from 'browser/main/lib/eventEmitter' import { findStorage } from 'browser/lib/findStorage' +import ConfigManager from 'browser/main/lib/ConfigManager' class MarkdownEditor extends React.Component { constructor (props) { @@ -18,7 +19,7 @@ class MarkdownEditor extends React.Component { this.supportMdSelectionBold = [16, 17, 186] this.state = { - status: 'PREVIEW', + status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW', renderValue: props.value, keyPressed: new Set(), isLocked: false @@ -64,6 +65,10 @@ class MarkdownEditor extends React.Component { }) } + setValue (value) { + this.refs.code.setValue(value) + } + handleChange (e) { this.value = this.refs.code.value this.props.onChange(e) @@ -72,9 +77,7 @@ class MarkdownEditor extends React.Component { handleContextMenu (e) { const { config } = this.props if (config.editor.switchPreview === 'RIGHTCLICK') { - const newStatus = this.state.status === 'PREVIEW' - ? 'CODE' - : 'PREVIEW' + const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW' this.setState({ status: newStatus }, () => { @@ -84,6 +87,10 @@ class MarkdownEditor extends React.Component { this.refs.preview.focus() } eventEmitter.emit('topbar:togglelockbutton', this.state.status) + + const newConfig = Object.assign({}, config) + newConfig.editor.delfaultStatus = newStatus + ConfigManager.set(newConfig) }) } } @@ -140,8 +147,10 @@ class MarkdownEditor extends React.Component { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ - const checkedMatch = /\[x\]/i - const uncheckedMatch = /\[ \]/ + const checkedMatch = /^\s*[\+\-\*] \[x\]/i + const uncheckedMatch = /^\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 @@ -150,10 +159,10 @@ class MarkdownEditor extends React.Component { const targetLine = lines[lineIndex] if (targetLine.match(checkedMatch)) { - lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]') + lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { - lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]') + lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') } this.refs.code.setValue(lines.join('\n')) } @@ -250,7 +259,7 @@ class MarkdownEditor extends React.Component { : 'codeEditor--hide' } ref='code' - mode='GitHub Flavored Markdown' + mode='Boost Flavored Markdown' value={value} theme={config.editor.theme} keyMap={config.editor.keyMap} @@ -268,6 +277,10 @@ class MarkdownEditor extends React.Component { enableTableEditor={config.editor.enableTableEditor} onChange={(e) => this.handleChange(e)} onBlur={(e) => this.handleBlur(e)} + spellCheck={config.editor.spellcheck} + enableSmartPaste={config.editor.enableSmartPaste} + hotkey={config.hotkey} + switchPreview={config.editor.switchPreview} /> ) diff --git a/browser/components/MarkdownEditor.styl b/browser/components/MarkdownEditor.styl index 13455e5d..c8fe2e49 100644 --- a/browser/components/MarkdownEditor.styl +++ b/browser/components/MarkdownEditor.styl @@ -16,7 +16,6 @@ .preview display block absolute top bottom left right - z-index 100 background-color white height 100% width 100% diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index b3d59b47..17d2cb82 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -17,9 +17,11 @@ import copy from 'copy-to-clipboard' 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 ConfigManager from '../main/lib/ConfigManager' const { remote, shell } = require('electron') const attachmentManagement = require('../main/lib/dataApi/attachmentManagement') @@ -80,7 +82,6 @@ function buildStyle ( url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'), url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype'); } -${allowCustomCSS ? customCSS : ''} ${markdownStyle} body { @@ -88,6 +89,11 @@ body { font-size: ${fontSize}px; ${scrollPastEnd && 'padding-bottom: 90vh;'} } +@media print { + body { + padding-bottom: initial; + } +} code { font-family: '${codeBlockFontFamily.join("','")}'; background-color: rgba(0,0,0,0.04); @@ -144,6 +150,8 @@ body p { display: none } } + +${allowCustomCSS ? customCSS : ''} ` } @@ -256,6 +264,10 @@ export default class MarkdownPreview extends React.Component { } handleMouseDown (e) { + const config = ConfigManager.get() + if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') { + eventEmitter.emit('topbar:togglemodebutton', 'CODE') + } if (e.target != null) { switch (e.target.tagName) { case 'A': @@ -279,26 +291,7 @@ export default class MarkdownPreview extends React.Component { } handleSaveAsMd () { - this.exportAsDocument('md', (noteContent, exportTasks) => { - let result = noteContent - if (this.props && this.props.storagePath && this.props.noteKey) { - const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( - noteContent, - this.props.storagePath - ) - attachmentsAbsolutePaths.forEach(attachment => { - exportTasks.push({ - src: attachment, - dst: attachmentManagement.DESTINATION_FOLDER - }) - }) - result = attachmentManagement.removeStorageAndNoteReferences( - noteContent, - this.props.noteKey - ) - } - return result - }) + this.exportAsDocument('md') } handleSaveAsHtml () { @@ -325,15 +318,8 @@ export default class MarkdownPreview extends React.Component { allowCustomCSS, customCSS ) - let body = this.markdown.render( - escapeHtmlCharacters(noteContent, { detectCodeBlock: true }) - ) + let body = this.markdown.render(noteContent) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] - const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( - noteContent, - this.props.storagePath - ) - files.forEach(file => { if (global.process.platform === 'win32') { file = file.replace('file:///', '') @@ -345,16 +331,6 @@ export default class MarkdownPreview extends React.Component { dst: 'css' }) }) - attachmentsAbsolutePaths.forEach(attachment => { - exportTasks.push({ - src: attachment, - dst: attachmentManagement.DESTINATION_FOLDER - }) - }) - body = attachmentManagement.removeStorageAndNoteReferences( - body, - this.props.noteKey - ) let styles = '' files.forEach(file => { @@ -387,8 +363,9 @@ export default class MarkdownPreview extends React.Component { if (filename) { const content = this.props.value const storage = this.props.storagePath + const nodeKey = this.props.noteKey - exportNote(storage, content, filename, contentFormatter) + exportNote(nodeKey, storage, content, filename, contentFormatter) .then(res => { dialog.showMessageBox(remote.getCurrentWindow(), { type: 'info', @@ -425,6 +402,7 @@ export default class MarkdownPreview extends React.Component { case 'dark': case 'solarized-dark': case 'monokai': + case 'dracula': return scrollBarDarkStyle default: return scrollBarStyle @@ -526,7 +504,8 @@ export default class MarkdownPreview extends React.Component { prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize || prevProps.smartArrows !== this.props.smartArrows || - prevProps.breaks !== this.props.breaks + prevProps.breaks !== this.props.breaks || + prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox ) { this.initMarkdown() this.rewriteIframe() @@ -732,7 +711,6 @@ export default class MarkdownPreview extends React.Component { el.addEventListener('click', this.linkClickHandler) }) } catch (e) { - console.error(e) el.className = 'flowchart-error' el.innerHTML = 'Flowchart parse error: ' + e.message } @@ -753,7 +731,6 @@ export default class MarkdownPreview extends React.Component { el.addEventListener('click', this.linkClickHandler) }) } catch (e) { - console.error(e) el.className = 'sequence-error' el.innerHTML = 'Sequence diagram parse error: ' + e.message } @@ -764,14 +741,21 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.querySelectorAll('.chart'), el => { try { - const chartConfig = JSON.parse(el.innerHTML) + const format = el.attributes.getNamedItem('data-format').value + const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML) el.innerHTML = '' - var canvas = document.createElement('canvas') + + const canvas = document.createElement('canvas') el.appendChild(canvas) - /* eslint-disable no-new */ - new Chart(canvas, chartConfig) + + const height = el.attributes.getNamedItem('data-height') + if (height && height.value !== 'undefined') { + el.style.height = height.value + 'vh' + canvas.height = height.value + 'vh' + } + + const chart = new Chart(canvas, chartConfig) } catch (e) { - console.error(e) el.className = 'chart-error' el.innerHTML = 'chartjs diagram parse error: ' + e.message } @@ -855,6 +839,15 @@ export default class MarkdownPreview extends React.Component { return } + const regexIsLine = /^:line:[0-9]/ + if (regexIsLine.test(linkHash)) { + const numberPattern = /\d+/g + + const lineNumber = parseInt(linkHash.match(numberPattern)[0]) + eventEmitter.emit('line:jump', lineNumber) + return + } + // this will match the old link format storage.key-note.key // e.g. // 877f99c3268608328037-1c211eb7dcb463de6490 diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index ddc9d7e0..bd79bc24 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -20,12 +20,18 @@ class MarkdownSplitEditor extends React.Component { } } + setValue (value) { + this.refs.code.setValue(value) + } + handleOnChange () { this.value = this.refs.code.value this.props.onChange() } handleScroll (e) { + if (!this.props.config.preview.scrollSync) return + const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document') const codeDoc = _.get(this, 'refs.code.editor.doc') let srcTop, srcHeight, targetTop, targetHeight @@ -72,8 +78,10 @@ class MarkdownSplitEditor extends React.Component { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ - const checkedMatch = /\[x\]/i - const uncheckedMatch = /\[ \]/ + const checkedMatch = /^\s*[\+\-\*] \[x\]/i + const uncheckedMatch = /^\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 @@ -82,10 +90,10 @@ class MarkdownSplitEditor extends React.Component { const targetLine = lines[lineIndex] if (targetLine.match(checkedMatch)) { - lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]') + lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { - lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]') + lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') } this.refs.code.setValue(lines.join('\n')) } @@ -145,7 +153,7 @@ class MarkdownSplitEditor extends React.Component { styleName='codeEditor' ref='code' width={this.state.codeEditorWidthInPercent + '%'} - mode='GitHub Flavored Markdown' + mode='Boost Flavored Markdown' value={value} theme={config.editor.theme} keyMap={config.editor.keyMap} @@ -163,6 +171,10 @@ class MarkdownSplitEditor extends React.Component { noteKey={noteKey} onChange={this.handleOnChange.bind(this)} onScroll={this.handleScroll.bind(this)} + spellCheck={config.editor.spellcheck} + enableSmartPaste={config.editor.enableSmartPaste} + hotkey={config.hotkey} + switchPreview={config.editor.switchPreview} />
this.handleMouseDown(e)} >
@@ -192,6 +204,7 @@ class MarkdownSplitEditor extends React.Component { noteKey={noteKey} customCSS={config.preview.customCSS} allowCustomCSS={config.preview.allowCustomCSS} + lineThroughCheckbox={config.preview.lineThroughCheckbox} />
) diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 600b7e2d..2fc70a39 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -24,16 +24,19 @@ const TagElement = ({ tagName }) => ( /** * @description Tag element list component. * @param {Array|null} tags + * @param {boolean} showTagsAlphabetically * @return {React.Component} */ -const TagElementList = tags => { +const TagElementList = (tags, showTagsAlphabetically) => { if (!isArray(tags)) { return [] } - const tagElements = tags.map(tag => TagElement({ tagName: tag })) - - return tagElements + if (showTagsAlphabetically) { + return _.sortBy(tags).map(tag => TagElement({ tagName: tag })) + } else { + return tags.map(tag => TagElement({ tagName: tag })) + } } /** @@ -55,7 +58,8 @@ const NoteItem = ({ pathname, storageName, folderName, - viewType + viewType, + showTagsAlphabetically }) => (
{note.tags.length > 0 - ? TagElementList(note.tags) + ? TagElementList(note.tags, showTagsAlphabetically) : (
{storageList.length > 0 ? storageList : ( -
No storage mount.
+
No storage mount.
)}
) StorageList.propTypes = { - storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired + storageList: PropTypes.arrayOf(PropTypes.element).isRequired } export default CSSModules(StorageList, styles) diff --git a/browser/components/TagListItem.js b/browser/components/TagListItem.js index 6cd50c9c..eec8ab14 100644 --- a/browser/components/TagListItem.js +++ b/browser/components/TagListItem.js @@ -14,8 +14,8 @@ import CSSModules from 'browser/lib/CSSModules' * @param {bool} isRelated */ -const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => ( -
+const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => ( +
handleContextMenu(e, name)}> {isRelated ?
diff --git a/browser/components/TodoListPercentage.js b/browser/components/TodoListPercentage.js index 3565f274..b917bbc1 100644 --- a/browser/components/TodoListPercentage.js +++ b/browser/components/TodoListPercentage.js @@ -12,7 +12,7 @@ import styles from './TodoListPercentage.styl' */ const TodoListPercentage = ({ - percentageOfTodo + percentageOfTodo, onClearCheckboxClick }) => (
@@ -20,11 +20,15 @@ const TodoListPercentage = ({

{percentageOfTodo}%

+
+

onClearCheckboxClick(e)}>clear

+
) TodoListPercentage.propTypes = { - percentageOfTodo: PropTypes.number.isRequired + percentageOfTodo: PropTypes.number.isRequired, + onClearCheckboxClick: PropTypes.func.isRequired } export default CSSModules(TodoListPercentage, styles) diff --git a/browser/components/TodoListPercentage.styl b/browser/components/TodoListPercentage.styl index 94e75599..5a0f3257 100644 --- a/browser/components/TodoListPercentage.styl +++ b/browser/components/TodoListPercentage.styl @@ -1,4 +1,5 @@ .percentageBar + display: flex position absolute top 72px right 0px @@ -30,6 +31,20 @@ color #f4f4f4 font-weight 600 +.todoClear + display flex + justify-content: flex-end + position absolute + z-index 120 + width 100% + height 100% + padding 2px 10px + +.todoClearText + color #f4f4f4 + cursor pointer + font-weight 500 + body[data-theme="dark"] .percentageBar background-color #444444 @@ -39,6 +54,9 @@ body[data-theme="dark"] .percentageText color $ui-dark-text-color + + .todoClearText + color $ui-dark-text-color body[data-theme="solarized-dark"] .percentageBar @@ -50,6 +68,9 @@ body[data-theme="solarized-dark"] .percentageText color #fdf6e3 + .todoClearText + color #fdf6e3 + body[data-theme="monokai"] .percentageBar background-color: $ui-monokai-borderColor @@ -58,4 +79,17 @@ body[data-theme="monokai"] background-color $ui-monokai-active-color .percentageText - color $ui-monokai-text-color \ No newline at end of file + color $ui-monokai-text-color + +body[data-theme="dracula"] + .percentageBar + background-color $ui-dracula-borderColor + + .progressBar + background-color: $ui-dracula-active-color + + .percentageText + color $ui-dracula-text-color + + .percentageText + color $ui-dracula-text-color diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index fb30742d..b7f219b8 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -80,6 +80,9 @@ li &.checked text-decoration line-through opacity 0.5 + &.taskListItem.checked + text-decoration line-through + opacity 0.5 div.math-rendered text-align center .math-failed @@ -206,41 +209,39 @@ code text-decoration none margin-right 2px pre - padding 0.5em !important + padding 0.5rem !important border solid 1px #D1D1D1 border-radius 5px overflow-x auto - margin 0 0 1em + margin 0 0 1rem display flex line-height 1.4em - &.flowchart, &.sequence, &.chart - display flex - justify-content center - background-color white - &.CodeMirror - height initial - flex-wrap wrap - &>code - flex 1 - overflow-x auto code background-color inherit margin 0 padding 0 border none border-radius 0 + &.CodeMirror + height initial + flex-wrap wrap + &>code + flex 1 + overflow-x auto + &.mermaid svg + max-width 100% !important &>span.filename - width 100% - border-radius: 5px 0px 0px 0px - margin -8px 100% 8px -8px - padding 0px 6px + margin -0.5rem 100% 0.5rem -0.5rem + padding 0.125rem 0.375rem background-color #777; color white + &:empty + display none &>span.lineNumber display none font-size 1em - padding 0.5em 0 - margin -0.5em 0.5em -0.5em -0.5em + padding 0.5rem 0 + margin -0.5rem 0.5rem -0.5rem -0.5rem border-right 1px solid text-align right border-top-left-radius 4px @@ -361,7 +362,7 @@ for name, val in admonition_types .admonition.{name} @extend $admonition border-left-color: val[color] - + .admonition.{name}>.admonition-title @extend $admonition-title border-bottom-color: .1rem solid rgba(val[color], 0.2) @@ -372,6 +373,49 @@ for name, val in admonition_types color: val[color] content: val[icon] +dl + margin 2rem 0 + padding 0 + display flex + width 100% + flex-wrap wrap + align-items flex-start + border-bottom 1px solid borderColor + background-color tableHeadBgColor + +dt + border-top 1px solid borderColor + font-weight bold + text-align right + overflow hidden + flex-basis 20% + padding 0.4rem 0.9rem + box-sizing border-box + +dd + border-top 1px solid borderColor + flex-basis 80% + padding 0.4rem 0.9rem + min-height 2.5rem + background-color $ui-noteDetail-backgroundColor + box-sizing border-box + +dd + dd + margin-left 20% + +pre.fence + flex-wrap wrap + + .chart, .flowchart, .mermaid, .sequence + display flex + justify-content center + background-color white + max-width 100% + flex-grow 1 + + canvas, svg + max-width 100% !important + themeDarkBackground = darken(#21252B, 10%) themeDarkText = #f9f9f9 themeDarkBorder = lighten(themeDarkBackground, 20%) @@ -422,6 +466,14 @@ body[data-theme="dark"] kbd background-color themeDarkBorder color themeDarkText + dl + border-color themeDarkBorder + background-color themeDarkTableHead + dt + border-color themeDarkBorder + dd + border-color themeDarkBorder + background-color themeDarkPreview themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%) @@ -449,6 +501,14 @@ body[data-theme="solarized-dark"] border-color themeSolarizedDarkTableBorder &:last-child border-right solid 1px themeSolarizedDarkTableBorder + dl + border-color themeDarkBorder + background-color themeSolarizedDarkTableHead + dt + border-color themeDarkBorder + dd + border-color themeDarkBorder + background-color $ui-solarized-dark-noteDetail-backgroundColor themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%) @@ -477,4 +537,49 @@ body[data-theme="monokai"] &:last-child border-right solid 1px themeMonokaiTableBorder kbd - background-color themeDarkBackground \ No newline at end of file + background-color themeDarkBackground + dl + border-color themeDarkBorder + background-color themeMonokaiTableHead + dt + border-color themeDarkBorder + dd + border-color themeDarkBorder + background-color $ui-monokai-noteDetail-backgroundColor + +themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor +themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%) +themeDraculaTableHead = themeDraculaTableEven +themeDraculaTableBorder = themeDarkBorder + +body[data-theme="dracula"] + color $ui-dracula-text-color + border-color themeDarkBorder + background-color $ui-dracula-noteDetail-backgroundColor + table + thead + tr + background-color themeDraculaTableHead + th + border-color themeDraculaTableBorder + &:last-child + border-right solid 1px themeDraculaTableBorder + tbody + tr:nth-child(2n + 1) + background-color themeDraculaTableOdd + tr:nth-child(2n) + background-color themeDraculaTableEven + td + border-color themeDraculaTableBorder + &:last-child + border-right solid 1px themeDraculaTableBorder + kbd + background-color themeDarkBackground + dl + border-color themeDarkBorder + background-color themeDraculaTableHead + dt + border-color themeDarkBorder + dd + border-color themeDarkBorder + background-color $ui-dracula-noteDetail-backgroundColor diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js index 12dce327..e28e06ea 100644 --- a/browser/components/render/MermaidRender.js +++ b/browser/components/render/MermaidRender.js @@ -2,8 +2,8 @@ import mermaidAPI from 'mermaid' // fixes bad styling in the mermaid dark theme const darkThemeStyling = ` -.loopText tspan { - fill: white; +.loopText tspan { + fill: white; }` function getRandomInt (min, max) { @@ -11,9 +11,9 @@ function getRandomInt (min, max) { } function getId () { - var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - var id = 'm-' - for (var i = 0; i < 7; i++) { + const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + let id = 'm-' + for (let i = 0; i < 7; i++) { id += pool[getRandomInt(0, 16)] } return id @@ -21,16 +21,20 @@ function getId () { function render (element, content, theme) { try { - let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' + const height = element.attributes.getNamedItem('data-height') + if (height && height.value !== 'undefined') { + element.style.height = height.value + 'vh' + } + const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula' mermaidAPI.initialize({ theme: isDarkTheme ? 'dark' : 'default', - themeCSS: isDarkTheme ? darkThemeStyling : '' + themeCSS: isDarkTheme ? darkThemeStyling : '', + useMaxWidth: false }) mermaidAPI.render(getId(), content, (svgGraph) => { element.innerHTML = svgGraph }) } catch (e) { - console.error(e) element.className = 'mermaid-error' element.innerHTML = 'mermaid diagram parse error: ' + e.message } diff --git a/browser/lib/Languages.js b/browser/lib/Languages.js index ddb7e0ed..8c3747a9 100644 --- a/browser/lib/Languages.js +++ b/browser/lib/Languages.js @@ -48,8 +48,12 @@ const languages = [ locale: 'pl' }, { - name: 'Portuguese', - locale: 'pt' + name: 'Portuguese (PT-BR)', + locale: 'pt-BR' + }, + { + name: 'Portuguese (PT-PT)', + locale: 'pt-PT' }, { name: 'Russian', @@ -61,6 +65,9 @@ const languages = [ }, { name: 'Turkish', locale: 'tr' + }, { + name: 'Thai', + locale: 'th' } ] diff --git a/browser/lib/contextMenuBuilder.js b/browser/lib/contextMenuBuilder.js new file mode 100644 index 00000000..cf92f52e --- /dev/null +++ b/browser/lib/contextMenuBuilder.js @@ -0,0 +1,65 @@ +const {remote} = require('electron') +const {Menu} = remote.require('electron') +const spellcheck = require('./spellcheck') + +/** + * Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note. + * If the word is does not contains a spelling error (determined by the 'error style'), no suggestions for corrections are requested + * => they are not visible in the context menu + * @param editor CodeMirror editor + * @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) { + return null + } + 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) || [] + let isMisspelled = false + for (const mark of existingMarks) { + if (mark.className === spellcheck.getCSSClassName()) { + isMisspelled = true + break + } + } + let suggestion = [] + if (isMisspelled) { + suggestion = spellcheck.getSpellingSuggestion(word) + } + + const selection = { + isMisspelled: isMisspelled, + spellingSuggestions: suggestion + } + 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) + } + } + } + }).concat({ + type: 'separator' + })) + } + return Menu.buildFromTemplate(template) +} + +module.exports = buildEditorContextMenu diff --git a/browser/lib/findNoteTitle.js b/browser/lib/findNoteTitle.js index b954f172..912c3bdd 100644 --- a/browser/lib/findNoteTitle.js +++ b/browser/lib/findNoteTitle.js @@ -1,4 +1,4 @@ -export function findNoteTitle (value) { +export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') { const splitted = value.split('\n') let title = null let isInsideCodeBlock = false @@ -6,6 +6,11 @@ export function findNoteTitle (value) { if (splitted[0] === '---') { let line = 0 while (++line < splitted.length) { + if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) { + title = splitted[line].substring(frontMatterTitleField.length + 1).trim() + + break + } if (splitted[line] === '---') { splitted.splice(0, line + 1) @@ -14,17 +19,19 @@ export function findNoteTitle (value) { } } - splitted.some((line, index) => { - const trimmedLine = line.trim() - const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim() - if (trimmedLine.match('```')) { - isInsideCodeBlock = !isInsideCodeBlock - } - if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) { - title = trimmedLine - return true - } - }) + if (title === null) { + splitted.some((line, index) => { + const trimmedLine = line.trim() + const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim() + if (trimmedLine.match('```')) { + isInsideCodeBlock = !isInsideCodeBlock + } + if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) { + title = trimmedLine + return true + } + }) + } if (title === null) { title = '' diff --git a/browser/lib/markdown-it-deflist.js b/browser/lib/markdown-it-deflist.js new file mode 100644 index 00000000..db14c636 --- /dev/null +++ b/browser/lib/markdown-it-deflist.js @@ -0,0 +1,232 @@ +'use strict' + +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) { + let start = state.bMarks[line] + state.tShift[line] + const max = state.eMarks[line] + + if (start >= max) { return -1 } + + // Check bullet + const marker = state.src.charCodeAt(start++) + if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 } + + const pos = state.skipSpaces(start) + + // require space after ":" + if (start === pos) { return -1 } + + return start + } + + 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') { + state.tokens[i + 2].hidden = true + state.tokens[i].hidden = true + i += 2 + } + } + } + + function deflist (state, startLine, endLine, silent) { + var ch, + contentStart, + ddLine, + dtLine, + itemLines, + listLines, + listTokIdx, + max, + newEndLine, + nextLine, + offset, + oldDDIndent, + oldIndent, + oldLineMax, + oldParentType, + oldSCount, + oldTShift, + oldTight, + pos, + prevEmptyEnd, + tight, + token + + if (silent) { + // quirk: validation mode validates a dd block only, not a whole deflist + if (state.ddIndent < 0) { return false } + return skipMarker(state, startLine) >= 0 + } + + nextLine = startLine + 1 + if (nextLine >= endLine) { return false } + + if (state.isEmpty(nextLine)) { + nextLine++ + if (nextLine >= endLine) { return false } + } + + if (state.sCount[nextLine] < state.blkIndent) { return false } + contentStart = skipMarker(state, nextLine) + 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 ] + + // + // Iterate list items + // + + dtLine = startLine + ddLine = nextLine + + // One definition list can contain multiple DTs, + // and one DT can be followed by multiple DDs. + // + // Thus, there is two loops here, and label is + // needed to break out of the second one + // + /* eslint no-labels:0,block-scoped-var:0 */ + OUTER: + for (;;) { + prevEmptyEnd = false + + token = state.push('dt_open', 'dt', 1) + 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.children = [] + + token = state.push('dt_close', 'dt', -1) + + for (;;) { + token = state.push('dd_open', 'dd', 1) + token.map = itemLines = [ ddLine, 0 ] + + pos = contentStart + max = state.eMarks[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 + } else { + offset++ + } + } else { + break + } + + pos++ + } + + contentStart = pos + + oldTight = state.tight + oldDDIndent = state.ddIndent + oldIndent = state.blkIndent + oldTShift = state.tShift[ddLine] + oldSCount = state.sCount[ddLine] + oldParentType = state.parentType + state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2 + state.tShift[ddLine] = contentStart - state.bMarks[ddLine] + state.sCount[ddLine] = offset + state.tight = true + state.parentType = 'deflist' + + newEndLine = ddLine + while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) { + } + + oldLineMax = state.lineMax + state.lineMax = newEndLine + + state.md.block.tokenize(state, ddLine, newEndLine, true) + + state.lineMax = oldLineMax + + // If any of list item is tight, mark list as tight + if (!state.tight || prevEmptyEnd) { + tight = false + } + // 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) + + state.tShift[ddLine] = oldTShift + state.sCount[ddLine] = oldSCount + state.tight = oldTight + state.parentType = oldParentType + state.blkIndent = oldIndent + state.ddIndent = oldDDIndent + + token = state.push('dd_close', 'dd', -1) + + itemLines[1] = nextLine = state.line + + if (nextLine >= endLine) { break OUTER } + + if (state.sCount[nextLine] < state.blkIndent) { break OUTER } + contentStart = skipMarker(state, nextLine) + if (contentStart < 0) { break } + + ddLine = nextLine + + // go to the next loop iteration: + // insert DD tag and repeat checking + } + + if (nextLine >= endLine) { break } + dtLine = nextLine + + 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 (state.sCount[ddLine] < state.blkIndent) { break } + contentStart = skipMarker(state, ddLine) + if (contentStart < 0) { break } + + // go to the next loop iteration: + // insert DT and DD tags and repeat checking + } + + // Finilize list + token = state.push('dl_close', 'dl', -1) + + listLines[1] = nextLine + + state.line = nextLine + + // mark paragraphs tight if needed + if (tight) { + markTightParagraphs(state, listTokIdx) + } + + return true + } + + md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] }) +} diff --git a/browser/lib/markdown-it-fence.js b/browser/lib/markdown-it-fence.js new file mode 100644 index 00000000..f2f7e999 --- /dev/null +++ b/browser/lib/markdown-it-fence.js @@ -0,0 +1,136 @@ +'use strict' + +module.exports = function (md, renderers, defaultRenderer) { + const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/ + + function fence (state, startLine, endLine, silent) { + let pos = state.bMarks[startLine] + state.tShift[startLine] + let max = state.eMarks[startLine] + + if (state.sCount[startLine] - state.blkIndent >= 4 || pos + 3 > max) { + return false + } + + const marker = state.src.charCodeAt(pos) + if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) { + return false + } + + let mem = pos + pos = state.skipChars(pos, marker) + + let len = pos - mem + if (len < 3) { + return false + } + + const markup = state.src.slice(mem, pos) + const params = state.src.slice(pos, max) + + if (silent) { + return true + } + + let nextLine = startLine + let haveEndMarker = false + + while (true) { + nextLine++ + if (nextLine >= endLine) { + break + } + + pos = mem = state.bMarks[nextLine] + state.tShift[nextLine] + max = state.eMarks[nextLine] + + if (pos < max && state.sCount[nextLine] < state.blkIndent) { + break + } + if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) { + continue + } + + pos = state.skipChars(pos, marker) + + if (pos - mem < len) { + continue + } + + pos = state.skipSpaces(pos) + + if (pos >= max) { + haveEndMarker = true + break + } + } + + len = state.sCount[startLine] + state.line = nextLine + (haveEndMarker ? 1 : 0) + + const parameters = {} + let langType = '' + let fileName = '' + let firstLineNumber = 1 + + let match = paramsRE.exec(params) + if (match) { + if (match[1]) { + langType = match[1] + } + if (match[3]) { + fileName = match[3] + } + if (match[4]) { + firstLineNumber = parseInt(match[4], 10) + } + + if (match[2]) { + const params = match[2] + const regex = /(\w[-\w]*)(?:=(?:'(.*?[^\\])?'|"(.*?[^\\])?"|([^'"][^\s]*)))?/g + + let name, value + while ((match = regex.exec(params))) { + name = match[1] + value = match[2] || match[3] || match[4] || null + + const height = /^(\d+)h$/.exec(name) + if (height && !value) { + parameters.height = height[1] + } else { + parameters[name] = value + } + } + } + } + + let token + if (renderers[langType]) { + token = state.push(`${langType}_fence`, 'div', 0) + } else { + token = state.push('_fence', 'code', 0) + } + + token.langType = langType + token.fileName = fileName + token.firstLineNumber = firstLineNumber + token.parameters = parameters + + token.content = state.getLines(startLine + 1, nextLine, len, true) + token.markup = markup + token.map = [startLine, state.line] + + return true + } + + md.block.ruler.before('fence', '_fence', fence, { + alt: ['paragraph', 'reference', 'blockquote', 'list'] + }) + + for (const name in renderers) { + md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index]) + } + + if (defaultRenderer) { + md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index]) + } +} diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index 05e5e7be..8f6d86a8 100644 --- a/browser/lib/markdown-it-sanitize-html.js +++ b/browser/lib/markdown-it-sanitize-html.js @@ -2,6 +2,7 @@ import sanitizeHtml from 'sanitize-html' import { escapeHtmlCharacters } from './utils' +import url from 'url' module.exports = function sanitizePlugin (md, options) { options = options || {} @@ -14,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) { options ) } - if (state.tokens[tokenIdx].type === 'fence') { + if (state.tokens[tokenIdx].type === '_fence') { // escapeHtmlCharacters has better performance state.tokens[tokenIdx].content = escapeHtmlCharacters( state.tokens[tokenIdx].content, @@ -25,7 +26,7 @@ module.exports = function sanitizePlugin (md, options) { const inlineTokens = state.tokens[tokenIdx].children for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) { if (inlineTokens[childIdx].type === 'html_inline') { - inlineTokens[childIdx].content = sanitizeHtml( + inlineTokens[childIdx].content = sanitizeInline( inlineTokens[childIdx].content, options ) @@ -35,3 +36,89 @@ 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 + +function sanitizeInline (html, options) { + let match = tagRegex.exec(html) + if (!match) { + return '' + } + + const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options + + if (match[1] !== undefined) { + // opening tag + const tag = match[1].toLowerCase() + if (allowedTags.indexOf(tag) === -1) { + return '' + } + + const attributes = match[2] + + let attrs = '' + let name + let value + + while ((match = attributesRegex.exec(attributes))) { + name = match[1].toLowerCase() + value = match[3] + + 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))) { + continue + } + } + + attrs += ` ${name}` + if (match[2]) { + attrs += `="${value}"` + } + } + } + + if (selfClosing.indexOf(tag) === -1) { + return '<' + tag + attrs + '>' + } else { + return '<' + tag + attrs + ' />' + } + } else { + // closing tag + if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) { + return html + } else { + return '' + } + } +} + +function naughtyHRef (href, options) { + // href = href.replace(/[\x00-\x20]+/g, '') + href = href.replace(/<\!\-\-.*?\-\-\>/g, '') + + const matches = href.match(/^([a-zA-Z]+)\:/) + if (!matches) { + if (href.match(/^[\/\\]{2}/)) { + return !options.allowProtocolRelative + } + + // No scheme + return false + } + + const scheme = matches[1].toLowerCase() + + return options.allowedSchemes.indexOf(scheme) === -1 +} + +function naughtyIFrame (src, options) { + try { + const parsed = url.parse(src, false, true) + + return options.allowedIframeHostnames.index(parsed.hostname) === -1 + } catch (e) { + return true + } +} diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js index 716be83a..eae448ec 100644 --- a/browser/lib/markdown-toc-generator.js +++ b/browser/lib/markdown-toc-generator.js @@ -5,6 +5,7 @@ import toc from 'markdown-toc' import diacritics from 'diacritics-map' import stripColor from 'strip-color' +import mdlink from 'markdown-link' const EOL = require('os').EOL @@ -42,6 +43,12 @@ function caseSensitiveSlugify (str) { return str } +function linkify (tok, text, slug, opts) { + var uniqeID = opts.num === 0 ? '' : '-' + opts.num + tok.content = mdlink(text, '#' + slug + uniqeID) + return tok +} + const TOC_MARKER_START = '' const TOC_MARKER_END = '' @@ -84,7 +91,7 @@ export function generateInEditor (editor) { * @returns generatedTOC String containing generated TOC */ export function generate (markdownText) { - const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify}) + const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify, linkify: linkify}) return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END } diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index ed4fbca1..2a7b66b0 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -7,6 +7,7 @@ import _ from 'lodash' import ConfigManager from 'browser/main/lib/ConfigManager' import katex from 'katex' import { lastFindInArray } from './utils' +import anchor from '@enyaxu/markdown-it-anchor' function createGutter (str, firstLineNumber) { if (Number.isNaN(firstLineNumber)) firstLineNumber = 1 @@ -27,32 +28,6 @@ class Markdown { html: true, xhtmlOut: true, breaks: config.preview.breaks, - highlight: function (str, lang) { - const delimiter = ':' - const langInfo = lang.split(delimiter) - const langType = langInfo[0] - const fileName = langInfo[1] || '' - const firstLineNumber = parseInt(langInfo[2], 10) - - if (langType === 'flowchart') { - return `
${str}
` - } - if (langType === 'sequence') { - return `
${str}
` - } - if (langType === 'chart') { - return `
${str}
` - } - if (langType === 'mermaid') { - return `
${str}
` - } - return '
' +
-          '' + fileName + '' +
-          createGutter(str, firstLineNumber) +
-          '' +
-          str +
-          '
' - }, sanitize: 'STRICT' } @@ -105,7 +80,11 @@ class Markdown { 'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'], 'input': ['type', 'id', 'checked'] }, - allowedIframeHostnames: ['www.youtube.com'] + allowedIframeHostnames: ['www.youtube.com'], + selfClosing: [ 'img', 'br', 'hr', 'input' ], + allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ], + allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ], + allowProtocolRelative: true }) } @@ -139,19 +118,60 @@ class Markdown { this.md.use(require('markdown-it-imsize')) this.md.use(require('markdown-it-footnote')) this.md.use(require('markdown-it-multimd-table')) - this.md.use(require('markdown-it-named-headers'), { - slugify: (header) => { - return encodeURI(header.trim() + this.md.use(anchor, { + slugify: (title) => { + var slug = encodeURI(title.trim() .replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '') .replace(/\s+/g, '-')) .replace(/\-+$/, '') + return slug } }) this.md.use(require('markdown-it-kbd')) - this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']}) + this.md.use(require('markdown-it-abbr')) + this.md.use(require('markdown-it-sub')) + this.md.use(require('markdown-it-sup')) + 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 `
+          ${token.fileName}
+          
${token.content}
+
` + }, + flowchart: token => { + return `
+          ${token.fileName}
+          
${token.content}
+
` + }, + mermaid: token => { + return `
+          ${token.fileName}
+          
${token.content}
+
` + }, + sequence: token => { + return `
+          ${token.fileName}
+          
${token.content}
+
` + } + }, token => { + return `
+        ${token.fileName}
+        ${createGutter(token.content, token.firstLineNumber)}
+        ${token.content}
+      
` + }) + const deflate = require('markdown-it-plantuml/lib/deflate') this.md.use(require('markdown-it-plantuml'), '', { generateSource: function (umlCode) { @@ -223,7 +243,11 @@ class Markdown { if (!liToken.attrs) { liToken.attrs = [] } - liToken.attrs.push(['class', 'taskListItem']) + if (config.preview.lineThroughCheckbox) { + liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`]) + } else { + liToken.attrs.push(['class', 'taskListItem']) + } } content = `` } @@ -248,9 +272,12 @@ class Markdown { this.md.renderer.render = (tokens, options, env) => { tokens.forEach((token) => { switch (token.type) { - case 'heading_open': - case 'paragraph_open': case 'blockquote_open': + case 'dd_open': + case 'dt_open': + case 'heading_open': + case 'list_item_open': + case 'paragraph_open': case 'table_open': token.attrPush(['data-line', token.map[0]]) } diff --git a/browser/lib/newNote.js b/browser/lib/newNote.js index bed69735..0b64d0e1 100644 --- a/browser/lib/newNote.js +++ b/browser/lib/newNote.js @@ -3,14 +3,21 @@ import dataApi from 'browser/main/lib/dataApi' import ee from 'browser/main/lib/eventEmitter' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' -export function createMarkdownNote (storage, folder, dispatch, location) { +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/)) { + tags = params.tagname.split(' ') + } + return dataApi .createNote(storage, { type: 'MARKDOWN_NOTE', folder: folder, title: '', + tags, content: '' }) .then(note => { @@ -29,14 +36,21 @@ export function createMarkdownNote (storage, folder, dispatch, location) { }) } -export function createSnippetNote (storage, folder, dispatch, location, 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/)) { + tags = params.tagname.split(' ') + } + return dataApi .createNote(storage, { type: 'SNIPPET_NOTE', folder: folder, title: '', + tags, description: '', snippets: [ { diff --git a/browser/lib/spellcheck.js b/browser/lib/spellcheck.js new file mode 100644 index 00000000..dd04e575 --- /dev/null +++ b/browser/lib/spellcheck.js @@ -0,0 +1,232 @@ +import styles from '../components/CodeEditor.styl' +import i18n from 'browser/lib/i18n' + +const Typo = require('typo-js') +const _ = require('lodash') + +const CSS_ERROR_CLASS = 'codeEditor-typo' +const SPELLCHECK_DISABLED = 'NONE' +const DICTIONARY_PATH = '../dictionaries' +const MILLISECONDS_TILL_LIVECHECK = 500 + +let dictionary = null +let self + +function getAvailableDictionaries () { + return [ + {label: i18n.__('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) { + dictionary = newDictionary +} + +/** + * @description Initializes the spellcheck. It removes all existing marks of the current editor. + * If a language was given (i.e. lang !== this.SPELLCHECK_DISABLED) it will load the stated dictionary and use it to check the whole document. + * @param {Codemirror} editor CodeMirror-Editor + * @param {String} lang on of the values from getAvailableDictionaries()-Method + */ +function setLanguage (editor, lang) { + self = this + dictionary = null + + if (editor == null) { + return + } + + const existingMarks = editor.getAllMarks() || [] + for (const mark of existingMarks) { + mark.clear() + } + if (lang !== SPELLCHECK_DISABLED) { + dictionary = new Typo(lang, false, false, { + dictionaryPath: DICTIONARY_PATH, + asyncLoad: true, + loadedCallback: () => + checkWholeDocument(editor) + }) + } +} + +/** + * Checks the whole content of the editor for typos + * @param {Codemirror} editor CodeMirror-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} + checkMultiLineRange(editor, from, to) +} + +/** + * Checks the given range for typos + * @param {Codemirror} editor CodeMirror-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} + } + return {from: pos1, to: pos2} + } + + 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 + if (l === smallerPos.line) { + w = smallerPos.ch + } + let wEnd = line.length + if (l === higherPos.line) { + wEnd = higherPos.ch + } + while (w <= wEnd) { + const wordRange = editor.findWordAt({line: l, ch: w}) + self.checkWord(editor, wordRange) + w += (wordRange.head.ch - wordRange.anchor.ch) + 1 + } + } +} + +/** + * @description Checks whether a certain range of characters in the editor (i.e. a word) contains a typo. + * If so the ranged will be marked with the class CSS_ERROR_CLASS. + * Note: Due to performance considerations, only words with more then 3 signs are checked. + * @param {Codemirror} editor CodeMirror-Editor + * @param wordRange Object specifying the range that should be checked. + * Having the following structure: {anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}} + */ +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]}) + } +} + +/** + * Checks the changes recently made (aka live check) + * @param {Codemirror} editor CodeMirror-Editor + * @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) { + /** + * 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) { + 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)) { + smallest = currentPos + } + if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) { + biggest = currentPos + } + } + return {start: smallest, end: biggest} + } + + if (dictionary === null || editor == null) { return } + + try { + const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject) + + // Expand the range to include words after/before whitespaces + start.ch = Math.max(start.ch - 1, 0) + end.ch = end.ch + 1 + + // clean existing marks + const existingMarks = editor.findMarks(start, end) || [] + for (const mark of existingMarks) { + mark.clear() + } + + 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) + } +} + +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 +}) + +/** + * 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) { + if (dictionary === null) { + return + } + debouncedSpellCheckLeading(changeObject) + debouncedSpellCheck(editor, liveSpellCheckFrom, changeObject) +} + +/** + * Returns an array of spelling suggestions for the given (wrong written) word. + * Returns an empty array if the dictionary is null (=> spellcheck is disabled) or the given word was null + * @param word word to be checked + * @returns {String[]} Array of suggestions + */ +function getSpellingSuggestion (word) { + if (dictionary == null || word == null) { + return [] + } + return dictionary.suggest(word) +} + +/** + * Returns the name of the CSS class used for errors + */ +function getCSSClassName () { + return styles[CSS_ERROR_CLASS] +} + +module.exports = { + DICTIONARY_PATH, + CSS_ERROR_CLASS, + SPELLCHECK_DISABLED, + getAvailableDictionaries, + setLanguage, + checkChangeRange, + handleChange, + getSpellingSuggestion, + checkWord, + checkMultiLineRange, + checkWholeDocument, + setDictionaryForTestsOnly, + getCSSClassName +} diff --git a/browser/main/Detail/Detail.styl b/browser/main/Detail/Detail.styl index 49a634f3..1b7bd606 100644 --- a/browser/main/Detail/Detail.styl +++ b/browser/main/Detail/Detail.styl @@ -23,7 +23,7 @@ body[data-theme="dark"] border-left 1px solid $ui-dark-borderColor .empty-message color $ui-dark-inactive-text-color - + body[data-theme="solarized-dark"] .root background-color $ui-solarized-dark-noteDetail-backgroundColor @@ -37,3 +37,10 @@ body[data-theme="monokai"] border-left 1px solid $ui-monokai-borderColor .empty-message color $ui-monokai-text-color + +body[data-theme="dracula"] + .root + background-color $ui-dracula-noteDetail-backgroundColor + border-left 1px solid $ui-dracula-borderColor + .empty-message + color $ui-dracula-text-color \ No newline at end of file diff --git a/browser/main/Detail/FolderSelect.styl b/browser/main/Detail/FolderSelect.styl index cfdc2734..fe045e3a 100644 --- a/browser/main/Detail/FolderSelect.styl +++ b/browser/main/Detail/FolderSelect.styl @@ -36,7 +36,7 @@ height 34px width 20px line-height 34px - + .search-input vertical-align middle position relative @@ -71,7 +71,7 @@ overflow ellipsis cursor pointer &:hover - background-color $ui-button--hover-backgroundColor + background-color $ui-button--hover-backgroundColor .search-optionList-item--active @extend .search-optionList-item @@ -159,3 +159,29 @@ body[data-theme="monokai"] color $ui-monokai-button--active-color .search-optionList-item-name-surfix color $ui-monokai-inactive-text-color + +body[data-theme="dracula"] + .root + color $ui-dracula-text-color + &:hover + color #f8f8f2 + background-color $ui-dark-button--hover-backgroundColor + border-color $ui-dracula-borderColor + + .search-optionList + color #f8f8f2 + border-color $ui-dracula-borderColor + background-color $ui-dracula-button-backgroundColor + + .search-optionList-item + &:hover + background-color lighten($ui-dracula-button--hover-backgroundColor, 15%) + + .search-optionList-item--active + background-color $ui-dracula-button--active-backgroundColor + color $ui-dracula-button--active-color + &:hover + background-color $ui-dark-button--hover-backgroundColor + color $ui-dracula-button--active-color + .search-optionList-item-name-surfix + color $ui-dracula-inactive-text-color diff --git a/browser/main/Detail/InfoPanel.js b/browser/main/Detail/InfoPanel.js index 4ce610fa..15535186 100644 --- a/browser/main/Detail/InfoPanel.js +++ b/browser/main/Detail/InfoPanel.js @@ -70,22 +70,22 @@ class InfoPanel extends React.Component {
- - - - diff --git a/browser/main/Detail/InfoPanel.styl b/browser/main/Detail/InfoPanel.styl index 2a73ca7e..1f774174 100644 --- a/browser/main/Detail/InfoPanel.styl +++ b/browser/main/Detail/InfoPanel.styl @@ -257,3 +257,43 @@ body[data-theme="monokai"] color $ui-dark-inactive-text-color &:hover color $ui-monokai-text-color + +body[data-theme="dracula"] + .control-infoButton-panel + background-color $ui-dracula-noteList-backgroundColor + + .control-infoButton-panel-trash + background-color $ui-dracula-noteList-backgroundColor + + .modification-date + color $ui-dracula-text-color + + .modification-date-desc + color $ui-inactive-text-color + + .infoPanel-defaul-count + color $ui-dracula-text-color + + .infoPanel-sub-count + color $ui-inactive-text-color + + .infoPanel-default + color $ui-dracula-text-color + + .infoPanel-sub + color $ui-inactive-text-color + + .infoPanel-noteLink + background-color alpha($ui-dracula-borderColor, 20%) + color $ui-dracula-text-color + + [id=export-wrap] + button + color $ui-dark-inactive-text-color + &:hover + background-color alpha($ui-dracula-borderColor, 20%) + color $ui-dracula-text-color + p + color $ui-dark-inactive-text-color + &:hover + color $ui-dracula-text-color \ No newline at end of file diff --git a/browser/main/Detail/InfoPanelTrashed.js b/browser/main/Detail/InfoPanelTrashed.js index db64a284..d4c8045d 100644 --- a/browser/main/Detail/InfoPanelTrashed.js +++ b/browser/main/Detail/InfoPanelTrashed.js @@ -31,17 +31,17 @@ const InfoPanelTrashed = ({
- - - diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index e4493a80..b4e7a5b3 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -61,11 +61,14 @@ class MarkdownNoteDetail extends React.Component { 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) { - if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) { + const isNewNote = nextProps.note.key !== this.props.note.key + 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({}, nextProps.note) @@ -91,7 +94,7 @@ class MarkdownNoteDetail extends React.Component { handleUpdateContent () { const { note } = this.state note.content = this.refs.content.value - note.title = markdown.strip(striptags(findNoteTitle(note.content))) + note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField))) this.updateNote(note) } @@ -187,6 +190,36 @@ class MarkdownNoteDetail extends React.Component { ee.emit('export:save-html') } + handleKeyDown (e) { + switch (e.keyCode) { + // tab key + case 9: + if (e.ctrlKey && !e.shiftKey) { + e.preventDefault() + this.jumpNextTab() + } else if (e.ctrlKey && e.shiftKey) { + e.preventDefault() + this.jumpPrevTab() + } else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) { + e.preventDefault() + this.focusEditor() + } + break + // I key + case 73: + { + const isSuper = global.process.platform === 'darwin' + ? e.metaKey + : e.ctrlKey + if (isSuper) { + e.preventDefault() + this.handleInfoButtonClick(e) + } + } + break + } + } + handleTrashButtonClick (e) { const { note } = this.state const { isTrashed } = note @@ -293,9 +326,33 @@ class MarkdownNoteDetail extends React.Component { }) } + handleDeleteNote () { + this.handleTrashButtonClick() + } + + 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') + + note.content = clearTodoContent + this.refs.content.setValue(note.content) + + this.updateNote(note) + } + renderEditor () { const { config, ignorePreviewPointerEvents } = this.props const { note } = this.state + if (this.state.editorType === 'EDITOR_PREVIEW') { return - + this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
this.handleSwitchMode(e)} editorType={editorType} /> @@ -429,6 +488,7 @@ class MarkdownNoteDetail extends React.Component {
this.handleKeyDown(e)} > {location.pathname === '/trashed' ? trashTopBar : detailTopBar} diff --git a/browser/main/Detail/MarkdownNoteDetail.styl b/browser/main/Detail/MarkdownNoteDetail.styl index b27dc80e..cdfeaf3a 100644 --- a/browser/main/Detail/MarkdownNoteDetail.styl +++ b/browser/main/Detail/MarkdownNoteDetail.styl @@ -76,3 +76,8 @@ body[data-theme="monokai"] .root border-left 1px solid $ui-monokai-borderColor background-color $ui-monokai-noteDetail-backgroundColor + +body[data-theme="dracula"] + .root + border-left 1px solid $ui-dracula-borderColor + background-color $ui-dracula-noteDetail-backgroundColor \ No newline at end of file diff --git a/browser/main/Detail/NoteDetailInfo.styl b/browser/main/Detail/NoteDetailInfo.styl index 7166a497..1ca46516 100644 --- a/browser/main/Detail/NoteDetailInfo.styl +++ b/browser/main/Detail/NoteDetailInfo.styl @@ -98,8 +98,13 @@ body[data-theme="solarized-dark"] .info border-color $ui-solarized-dark-borderColor background-color $ui-solarized-dark-noteDetail-backgroundColor - + body[data-theme="monokai"] .info border-color $ui-monokai-borderColor - background-color $ui-monokai-noteDetail-backgroundColor \ No newline at end of file + background-color $ui-monokai-noteDetail-backgroundColor + +body[data-theme="dracula"] + .info + border-color $ui-dracula-borderColor + background-color $ui-dracula-noteDetail-backgroundColor \ No newline at end of file diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 9356a02c..887e5237 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -112,7 +112,7 @@ class SnippetNoteDetail extends React.Component { if (this.refs.tags) note.tags = this.refs.tags.value note.description = this.refs.description.value note.updatedAt = new Date() - note.title = findNoteTitle(note.description) + note.title = findNoteTitle(note.description, false) this.setState({ note @@ -354,12 +354,10 @@ class SnippetNoteDetail extends React.Component { this.refs['code-' + this.state.snippetIndex].reload() if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) { - console.log('no need for arrows') this.moveTabBarBy(0) } else { const lastTab = this.allTabs.lastChild if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) { - console.log('need to scroll') const width = this.visibleTabs.offsetWidth const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width this.moveTabBarBy(newLeft > 0 ? -newLeft : 0) @@ -436,6 +434,18 @@ class SnippetNoteDetail extends React.Component { this.focusEditor() } break + // I key + case 73: + { + const isSuper = global.process.platform === 'darwin' + ? e.metaKey + : e.ctrlKey + if (isSuper) { + e.preventDefault() + this.handleInfoButtonClick(e) + } + } + break // L key case 76: { @@ -627,7 +637,6 @@ class SnippetNoteDetail extends React.Component { } focusEditor () { - console.log('code-' + this.state.snippetIndex) this.refs['code-' + this.state.snippetIndex].focus() } @@ -636,11 +645,18 @@ class SnippetNoteDetail extends React.Component { if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none' } - showWarning () { + showWarning (e, msg) { + const warningMessage = (msg) => ({ + 'export-txt': 'Text export', + 'export-md': 'Markdown export', + 'export-html': 'HTML export', + 'print': 'Print' + })[msg] + dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Sorry!'), - detail: i18n.__('md/text import is available only a markdown note.'), + detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'), buttons: [i18n.__('OK')] }) } @@ -708,6 +724,8 @@ class SnippetNoteDetail extends React.Component { enableTableEditor={config.editor.enableTableEditor} onChange={(e) => this.handleCodeChange(index)(e)} ref={'code-' + index} + enableSmartPaste={config.editor.enableSmartPaste} + hotkey={config.hotkey} /> }
@@ -759,6 +777,8 @@ class SnippetNoteDetail extends React.Component { this.handleChange(e)} /> @@ -789,7 +809,9 @@ class SnippetNoteDetail extends React.Component { createdAt={formatDate(note.createdAt)} exportAsMd={this.showWarning} exportAsTxt={this.showWarning} + exportAsHtml={this.showWarning} type={note.type} + print={this.showWarning} />
diff --git a/browser/main/Detail/SnippetNoteDetail.styl b/browser/main/Detail/SnippetNoteDetail.styl index f8ca48cc..e3bb31c6 100644 --- a/browser/main/Detail/SnippetNoteDetail.styl +++ b/browser/main/Detail/SnippetNoteDetail.styl @@ -169,4 +169,21 @@ body[data-theme="monokai"] .tabList background-color $ui-monokai-noteDetail-backgroundColor - color $ui-monokai-text-color \ No newline at end of file + color $ui-monokai-text-color + +body[data-theme="dracula"] + .root + border-left 1px solid $ui-dracula-borderColor + background-color $ui-dracula-noteDetail-backgroundColor + + .body + background-color $ui-dracula-noteDetail-backgroundColor + + .body .description textarea + background-color $ui-dracula-noteDetail-backgroundColor + color $ui-dracula-text-color + border 1px solid $ui-dracula-borderColor + + .tabList + background-color $ui-dracula-noteDetail-backgroundColor + color $ui-dracula-text-color \ No newline at end of file diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js index eb160e4c..6ced475b 100644 --- a/browser/main/Detail/TagSelect.js +++ b/browser/main/Detail/TagSelect.js @@ -179,10 +179,10 @@ class TagSelect extends React.Component { } render () { - const { value, className } = this.props + const { value, className, showTagsAlphabetically } = this.props const tagList = _.isArray(value) - ? value.map((tag) => { + ? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => { return ( ( -
-
onClick('SPLIT')}> - -
-
onClick('EDITOR_PREVIEW')}> - -
- {i18n.__('Toggle Mode')} -
-) - -ToggleModeButton.propTypes = { - onClick: PropTypes.func.isRequired, - editorType: PropTypes.string.Required -} - -export default CSSModules(ToggleModeButton, styles) +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './ToggleModeButton.styl' +import i18n from 'browser/lib/i18n' + +const ToggleModeButton = ({ + onClick, editorType +}) => ( +
+
onClick('SPLIT')}> + +
+
onClick('EDITOR_PREVIEW')}> + +
+ {i18n.__('Toggle Mode')} +
+) + +ToggleModeButton.propTypes = { + onClick: PropTypes.func.isRequired, + editorType: PropTypes.string.Required +} + +export default CSSModules(ToggleModeButton, styles) diff --git a/browser/main/Detail/ToggleModeButton.styl b/browser/main/Detail/ToggleModeButton.styl index 7ede0576..73f5acbd 100644 --- a/browser/main/Detail/ToggleModeButton.styl +++ b/browser/main/Detail/ToggleModeButton.styl @@ -63,3 +63,10 @@ body[data-theme="monokai"] .active background-color #f92672 box-shadow 2px 0px 7px #222222 + +body[data-theme="dracula"] + .control-toggleModeButton + background-color #44475a + .active + background-color #bd93f9 + box-shadow 2px 0px 7px #222222 diff --git a/browser/main/Main.js b/browser/main/Main.js index 65bde538..c426f2bd 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -80,7 +80,6 @@ class Main extends React.Component { } }) .then(data => { - console.log(data) store.dispatch({ type: 'ADD_STORAGE', storage: data.storage, @@ -141,7 +140,7 @@ class Main extends React.Component { componentDidMount () { const { dispatch, config } = this.props - const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai'] + const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai', 'dracula'] if (supportedThemes.indexOf(config.ui.theme) !== -1) { document.body.setAttribute('data-theme', config.ui.theme) @@ -168,6 +167,8 @@ class Main extends React.Component { } }) + delete CodeMirror.keyMap.emacs['Ctrl-V'] + eventEmitter.on('editor:fullscreen', this.toggleFullScreen) } @@ -297,7 +298,7 @@ class Main extends React.Component { onMouseUp={e => this.handleMouseUp(e)} > {!config.isSideNavFolded && diff --git a/browser/main/NewNoteButton/NewNoteButton.styl b/browser/main/NewNoteButton/NewNoteButton.styl index e8e4b5f0..75a9061c 100644 --- a/browser/main/NewNoteButton/NewNoteButton.styl +++ b/browser/main/NewNoteButton/NewNoteButton.styl @@ -79,3 +79,7 @@ body[data-theme="solarized-dark"] body[data-theme="monokai"] .root, .root--expanded background-color $ui-monokai-noteList-backgroundColor + +body[data-theme="dracula"] + .root, .root--expanded + background-color $ui-dracula-noteList-backgroundColor \ No newline at end of file diff --git a/browser/main/NewNoteButton/index.js b/browser/main/NewNoteButton/index.js index e739a550..c34443be 100644 --- a/browser/main/NewNoteButton/index.js +++ b/browser/main/NewNoteButton/index.js @@ -35,19 +35,20 @@ class NewNoteButton extends React.Component { } handleNewNoteButtonClick (e) { - const { location, dispatch, config } = this.props + const { location, params, dispatch, config } = this.props const { storage, folder } = this.resolveTargetFolder() if (config.ui.defaultNote === 'MARKDOWN_NOTE') { - createMarkdownNote(storage.key, folder.key, dispatch, location) + createMarkdownNote(storage.key, folder.key, dispatch, location, params, config) } else if (config.ui.defaultNote === 'SNIPPET_NOTE') { - createSnippetNote(storage.key, folder.key, dispatch, location, config) + createSnippetNote(storage.key, folder.key, dispatch, location, params, config) } else { modal.open(NewNoteModal, { storage: storage.key, folder: folder.key, dispatch, location, + params, config }) } diff --git a/browser/main/NoteList/NoteList.styl b/browser/main/NoteList/NoteList.styl index ea261208..73959c9b 100644 --- a/browser/main/NoteList/NoteList.styl +++ b/browser/main/NoteList/NoteList.styl @@ -84,7 +84,7 @@ body[data-theme="dark"] color $ui-dark-inactive-text-color &:hover color $ui-dark-text-color - + .control-button--active color $ui-dark-text-color &:active @@ -109,7 +109,7 @@ body[data-theme="solarized-dark"] color $ui-solarized-dark-inactive-text-color &:hover color $ui-solarized-dark-text-color - + .control-button--active color $ui-solarized-dark-text-color &:active @@ -138,3 +138,27 @@ body[data-theme="monokai"] color $ui-monokai-text-color &:active color $ui-monokai-text-color + +body[data-theme="dracula"] + .root + border-color $ui-dracula-borderColor + background-color $ui-dracula-noteList-backgroundColor + + .control + background-color $ui-dracula-noteList-backgroundColor + border-color $ui-dracula-borderColor + + .control-sortBy-select + &:hover + transition 0.2s + color $ui-dracula-text-color + + .control-button + color $ui-dracula-inactive-text-color + &:hover + color $ui-dracula-text-color + + .control-button--active + color $ui-dracula-text-color + &:active + color $ui-dracula-text-color \ No newline at end of file diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index a9818e4f..c4af06c6 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -55,7 +55,6 @@ class NoteList extends React.Component { super(props) this.selectNextNoteHandler = () => { - console.log('fired next') this.selectNextNote() } this.selectPriorNoteHandler = () => { @@ -64,13 +63,14 @@ class NoteList extends React.Component { this.focusHandler = () => { this.refs.list.focus() } - this.alertIfSnippetHandler = () => { - this.alertIfSnippet() + this.alertIfSnippetHandler = (event, msg) => { + this.alertIfSnippet(msg) } this.importFromFileHandler = this.importFromFile.bind(this) this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this) this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this) this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this) + this.cloneNote = this.cloneNote.bind(this) this.deleteNote = this.deleteNote.bind(this) this.focusNote = this.focusNote.bind(this) this.pinToTop = this.pinToTop.bind(this) @@ -83,7 +83,9 @@ class NoteList extends React.Component { // TODO: not Selected noteKeys but SelectedNote(for reusing) this.state = { + ctrlKeyDown: false, shiftKeyDown: false, + prevShiftNoteIndex: -1, selectedNoteKeys: [] } @@ -94,6 +96,7 @@ class NoteList extends React.Component { this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000) ee.on('list:next', this.selectNextNoteHandler) ee.on('list:prior', this.selectPriorNoteHandler) + ee.on('list:clone', this.cloneNote) ee.on('list:focus', this.focusHandler) ee.on('list:isMarkdownNote', this.alertIfSnippetHandler) ee.on('import:file', this.importFromFileHandler) @@ -116,6 +119,7 @@ class NoteList extends React.Component { ee.off('list:next', this.selectNextNoteHandler) ee.off('list:prior', this.selectPriorNoteHandler) + ee.off('list:clone', this.cloneNote) ee.off('list:focus', this.focusHandler) ee.off('list:isMarkdownNote', this.alertIfSnippetHandler) ee.off('import:file', this.importFromFileHandler) @@ -171,16 +175,15 @@ class NoteList extends React.Component { } } - focusNote (selectedNoteKeys, noteKey) { + focusNote (selectedNoteKeys, noteKey, pathname) { const { router } = this.context - const { location } = this.props this.setState({ selectedNoteKeys }) router.push({ - pathname: location.pathname, + pathname, query: { key: noteKey } @@ -199,6 +202,7 @@ class NoteList extends React.Component { } let { selectedNoteKeys } = this.state const { shiftKeyDown } = this.state + const { location } = this.props let targetIndex = this.getTargetIndex() @@ -215,7 +219,7 @@ class NoteList extends React.Component { selectedNoteKeys.push(priorNoteKey) } - this.focusNote(selectedNoteKeys, priorNoteKey) + this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname) ee.emit('list:moved') } @@ -226,6 +230,7 @@ class NoteList extends React.Component { } let { selectedNoteKeys } = this.state const { shiftKeyDown } = this.state + const { location } = this.props let targetIndex = this.getTargetIndex() const isTargetLastNote = targetIndex === this.notes.length - 1 @@ -248,7 +253,7 @@ class NoteList extends React.Component { selectedNoteKeys.push(nextNoteKey) } - this.focusNote(selectedNoteKeys, nextNoteKey) + this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname) ee.emit('list:moved') } @@ -260,13 +265,13 @@ class NoteList extends React.Component { } const selectedNoteKeys = [noteHash] - this.focusNote(selectedNoteKeys, noteHash) + this.focusNote(selectedNoteKeys, noteHash, '/home') ee.emit('list:moved') } handleNoteListKeyDown (e) { - if (e.metaKey || e.ctrlKey) return true + if (e.metaKey) return true // A key if (e.keyCode === 65 && !e.shiftKey) { @@ -274,12 +279,6 @@ class NoteList extends React.Component { ee.emit('top:new-note') } - // D key - if (e.keyCode === 68) { - e.preventDefault() - this.deleteNote() - } - // E key if (e.keyCode === 69) { e.preventDefault() @@ -306,6 +305,8 @@ class NoteList extends React.Component { if (e.shiftKey) { this.setState({ shiftKeyDown: true }) + } else if (e.ctrlKey) { + this.setState({ ctrlKeyDown: true }) } } @@ -313,6 +314,10 @@ class NoteList extends React.Component { if (!e.shiftKey) { this.setState({ shiftKeyDown: false }) } + + if (!e.ctrlKey) { + this.setState({ ctrlKeyDown: false }) + } } getNotes () { @@ -389,25 +394,65 @@ class NoteList extends React.Component { return pinnedNotes.concat(unpinnedNotes) } + getNoteIndexByKey (noteKey) { + return this.notes.findIndex((note) => { + if (!note) return -1 + + return note.key === noteKey + }) + } + handleNoteClick (e, uniqueKey) { const { router } = this.context const { location } = this.props - let { selectedNoteKeys } = this.state - const { shiftKeyDown } = this.state + let { selectedNoteKeys, prevShiftNoteIndex } = this.state + const { ctrlKeyDown, shiftKeyDown } = this.state + const hasSelectedNoteKey = selectedNoteKeys.length > 0 - if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) { + if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) { const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey) this.setState({ selectedNoteKeys: newSelectedNoteKeys }) return } - if (!shiftKeyDown) { + if (!ctrlKeyDown && !shiftKeyDown) { selectedNoteKeys = [] } + + if (!shiftKeyDown) { + prevShiftNoteIndex = -1 + } + selectedNoteKeys.push(uniqueKey) + + if (shiftKeyDown && hasSelectedNoteKey) { + let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0]) + // Shift selection can either start from first note in the exisiting selectedNoteKeys + // or previous first shift note index + firstShiftNoteIndex = firstShiftNoteIndex > prevShiftNoteIndex + ? firstShiftNoteIndex : prevShiftNoteIndex + + const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey) + + const startIndex = firstShiftNoteIndex < lastShiftNoteIndex + ? firstShiftNoteIndex : lastShiftNoteIndex + const endIndex = firstShiftNoteIndex > lastShiftNoteIndex + ? firstShiftNoteIndex : lastShiftNoteIndex + + selectedNoteKeys = [] + for (let i = startIndex; i <= endIndex; i++) { + selectedNoteKeys.push(this.notes[i].key) + } + + if (prevShiftNoteIndex < 0) { + prevShiftNoteIndex = firstShiftNoteIndex + } + } + this.setState({ - selectedNoteKeys + selectedNoteKeys, + prevShiftNoteIndex }) router.push({ @@ -446,14 +491,21 @@ class NoteList extends React.Component { }) } - alertIfSnippet () { + alertIfSnippet (msg) { + const warningMessage = (msg) => ({ + 'export-txt': 'Text export', + 'export-md': 'Markdown export', + 'export-html': 'HTML export', + 'print': 'Print' + })[msg] + const targetIndex = this.getTargetIndex() if (this.notes[targetIndex].type === 'SNIPPET_NOTE') { dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Sorry!'), - detail: i18n.__('md/text import is available only a markdown note.'), - buttons: [i18n.__('OK'), i18n.__('Cancel')] + detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'), + buttons: [i18n.__('OK')] }) } } @@ -615,7 +667,6 @@ class NoteList extends React.Component { .catch((err) => { console.error('Cannot Delete note: ' + err) }) - console.log('Notes were all deleted') } else { if (!confirmDeleteNote(confirmDeletion, false)) return @@ -635,7 +686,6 @@ class NoteList extends React.Component { }) }) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE') - console.log('Notes went to trash') }) .catch((err) => { console.error('Notes could not go to trash: ' + err) @@ -995,6 +1045,7 @@ class NoteList extends React.Component { folderName={this.getNoteFolder(note).name} storageName={this.getNoteStorage(note).name} viewType={viewType} + showTagsAlphabetically={config.ui.showTagsAlphabetically} /> ) } diff --git a/browser/main/SideNav/SideNav.styl b/browser/main/SideNav/SideNav.styl index ecab70d0..9fa6d4fa 100644 --- a/browser/main/SideNav/SideNav.styl +++ b/browser/main/SideNav/SideNav.styl @@ -19,7 +19,7 @@ text-align center - + .top-menu-label margin-left 5px overflow ellipsis @@ -122,3 +122,8 @@ body[data-theme="monokai"] .root, .root--folded background-color $ui-monokai-backgroundColor border-right 1px solid $ui-monokai-borderColor + +body[data-theme="dracula"] + .root, .root--folded + background-color $ui-dracula-backgroundColor + border-right 1px solid $ui-dracula-borderColor \ No newline at end of file diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index d17314b3..e336f3ce 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -25,7 +25,8 @@ class StorageItem extends React.Component { const { storage } = this.props this.state = { - isOpen: !!storage.isOpen + isOpen: !!storage.isOpen, + draggedOver: null } } @@ -204,6 +205,20 @@ class StorageItem extends React.Component { folderKey: data.folderKey, fileType: data.fileType }) + return data + }) + .then(data => { + dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'info', + message: 'Exported to "' + data.exportDir + '"' + }) + }) + .catch(err => { + dialog.showErrorBox( + 'Export error', + err ? err.message || err : 'Unexpected error during export' + ) + throw err }) } }) @@ -231,14 +246,20 @@ class StorageItem extends React.Component { } } - handleDragEnter (e) { - e.dataTransfer.setData('defaultColor', e.target.style.backgroundColor) - e.target.style.backgroundColor = 'rgba(129, 130, 131, 0.08)' + handleDragEnter (e, key) { + e.preventDefault() + if (this.state.draggedOver === key) { return } + this.setState({ + draggedOver: key + }) } handleDragLeave (e) { - e.target.style.opacity = '1' - e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor') + e.preventDefault() + if (this.state.draggedOver === null) { return } + this.setState({ + draggedOver: null + }) } dropNote (storage, folder, dispatch, location, noteData) { @@ -263,8 +284,12 @@ class StorageItem extends React.Component { } handleDrop (e, storage, folder, dispatch, location) { - e.target.style.opacity = '1' - e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor') + e.preventDefault() + if (this.state.draggedOver !== null) { + this.setState({ + draggedOver: null + }) + } const noteData = JSON.parse(e.dataTransfer.getData('note')) this.dropNote(storage, folder, dispatch, location, noteData) } @@ -274,7 +299,7 @@ class StorageItem extends React.Component { const { folderNoteMap, trashedSet } = data const SortableStorageItemChild = SortableElement(StorageItemChild) const folderList = storage.folders.map((folder, index) => { - let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key) + const folderRegex = new RegExp(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) @@ -291,16 +316,22 @@ class StorageItem extends React.Component { this.handleFolderButtonClick(folder.key)(e)} handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)} folderName={folder.name} folderColor={folder.color} isFolded={isFolded} noteCount={noteCount} - handleDrop={(e) => this.handleDrop(e, storage, folder, dispatch, location)} - handleDragEnter={this.handleDragEnter} - handleDragLeave={this.handleDragLeave} + handleDrop={(e) => { + this.handleDrop(e, storage, folder, dispatch, location) + }} + handleDragEnter={(e) => { + this.handleDragEnter(e, folder.key) + }} + handleDragLeave={(e) => { + this.handleDragLeave(e, folder) + }} /> ) }) diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 977a8fb5..b98d859d 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -18,6 +18,12 @@ import TagButton from './TagButton' import {SortableContainer} from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' import context from 'browser/lib/context' +import { remote } from 'electron' +import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' + +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 @@ -30,6 +36,52 @@ class SideNav extends React.Component { EventEmitter.off('side:preferences', this.handleMenuButtonClick) } + deleteTag (tag) { + const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), { + ype: '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, params } = this.props + + const notes = data.noteMap + .map(note => note) + .filter(note => note.tags.indexOf(tag) !== -1) + .map(note => { + note = Object.assign({}, note) + note.tags = note.tags.slice() + + note.tags.splice(note.tags.indexOf(tag), 1) + + return 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) + + this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`) + } + } + }) + } + } + handleMenuButtonClick (e) { openModal(PreferencesModal) } @@ -44,6 +96,17 @@ class SideNav extends React.Component { router.push('/starred') } + handleTagContextMenu (e, tag) { + const menu = [] + + menu.push({ + label: i18n.__('Delete Tag'), + click: this.deleteTag.bind(this, tag) + }) + + context.popup(menu) + } + handleToggleButtonClick (e) { const { dispatch, config } = this.props @@ -144,12 +207,20 @@ class SideNav extends React.Component { tagListComponent () { const { data, location, config } = this.props - const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap) + 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) }) - ), ['name']).filter( + ).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 + return tag + }) + } if (config.sortTagsBy === 'COUNTER') { tagList = _.sortBy(tagList, item => (0 - item.size)) } @@ -165,6 +236,7 @@ class SideNav extends React.Component { 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)} isRelated={tag.related} key={tag.name} @@ -198,7 +270,7 @@ class SideNav extends React.Component { const tags = pathSegments[pathSegments.length - 1] return (tags === 'alltags') ? [] - : tags.split(' ').map(tag => decodeURIComponent(tag)) + : decodeURIComponent(tags).split(' ') } handleClickTagListItem (name) { @@ -230,7 +302,7 @@ class SideNav extends React.Component { } else { listOfTags.push(tag) } - router.push(`/tags/${listOfTags.map(tag => encodeURIComponent(tag)).join(' ')}`) + router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`) } emptyTrash (entries) { @@ -238,6 +310,8 @@ class SideNav extends React.Component { 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 }) => { @@ -247,7 +321,6 @@ class SideNav extends React.Component { .catch((err) => { console.error('Cannot Delete note: ' + err) }) - console.log('Trash emptied') } handleFilterButtonContextMenu (event) { diff --git a/browser/main/StatusBar/StatusBar.styl b/browser/main/StatusBar/StatusBar.styl index 52cc4b02..23dec208 100644 --- a/browser/main/StatusBar/StatusBar.styl +++ b/browser/main/StatusBar/StatusBar.styl @@ -34,7 +34,7 @@ color $ui-active-color span margin-left 5px - + .update navButtonColor() height 24px @@ -47,6 +47,14 @@ .update-icon color $brand-color +body[data-theme="default"] + .zoom + color $ui-text-color + +body[data-theme="white"] + .zoom + color $ui-text-color + body[data-theme="dark"] .root border-color $ui-dark-borderColor @@ -80,3 +88,14 @@ body[data-theme="monokai"] color $ui-monokai-active-color &:active color $ui-monokai-active-color + +body[data-theme="dracula"] + navButtonColor() + .zoom + border-color $ui-dark-borderColor + color $ui-dracula-text-color + &:hover + transition 0.15s + color $ui-dracula-active-color + &:active + color $ui-dracula-active-color \ No newline at end of file diff --git a/browser/main/StatusBar/index.js b/browser/main/StatusBar/index.js index 8b48e3d3..c99bf036 100644 --- a/browser/main/StatusBar/index.js +++ b/browser/main/StatusBar/index.js @@ -5,6 +5,7 @@ import styles from './StatusBar.styl' import ZoomManager from 'browser/main/lib/ZoomManager' import i18n from 'browser/lib/i18n' import context from 'browser/lib/context' +import EventEmitter from 'browser/main/lib/eventEmitter' const electron = require('electron') const { remote, ipcRenderer } = electron @@ -13,6 +14,26 @@ 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] class StatusBar extends React.Component { + + constructor (props) { + super(props) + this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this) + this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this) + this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this) + } + + componentDidMount () { + EventEmitter.on('status:zoomin', this.handleZoomInMenuItem) + EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem) + EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem) + } + + componentWillUnmount () { + EventEmitter.off('status:zoomin', this.handleZoomInMenuItem) + EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem) + EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem) + } + updateApp () { const index = dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', @@ -48,6 +69,20 @@ class StatusBar extends React.Component { }) } + handleZoomInMenuItem () { + const zoomFactor = ZoomManager.getZoom() + 0.1 + this.handleZoomMenuItemClick(zoomFactor) + } + + handleZoomOutMenuItem () { + const zoomFactor = ZoomManager.getZoom() - 0.1 + this.handleZoomMenuItemClick(zoomFactor) + } + + handleZoomResetMenuItem () { + this.handleZoomMenuItemClick(1.0) + } + render () { const { config, status } = this.context diff --git a/browser/main/TopBar/TopBar.styl b/browser/main/TopBar/TopBar.styl index 7654f66f..61b21fc5 100644 --- a/browser/main/TopBar/TopBar.styl +++ b/browser/main/TopBar/TopBar.styl @@ -256,3 +256,25 @@ body[data-theme="monokai"] input background-color $ui-monokai-noteList-backgroundColor color $ui-monokai-text-color + +body[data-theme="dracula"] + .root, .root--expanded + background-color $ui-dracula-noteList-backgroundColor + + .control + border-color $ui-dracula-borderColor + .control-search + background-color $ui-dracula-noteList-backgroundColor + + .control-search-icon + absolute top bottom left + line-height 32px + width 35px + color $ui-dracula-inactive-text-color + background-color $ui-dracula-noteList-backgroundColor + + .control-search-input + background-color $ui-dracula-noteList-backgroundColor + input + background-color $ui-dracula-noteList-backgroundColor + color $ui-dracula-text-color \ No newline at end of file diff --git a/browser/main/global.styl b/browser/main/global.styl index 815cff4e..e04060c2 100644 --- a/browser/main/global.styl +++ b/browser/main/global.styl @@ -18,6 +18,9 @@ body ::-webkit-scrollbar width 12px +::-webkit-scrollbar-corner + background-color: transparent; + ::-webkit-scrollbar-thumb background-color rgba(0, 0, 0, 0.15) @@ -162,6 +165,15 @@ body[data-theme="monokai"] .sortableItemHelper color: $ui-monokai-text-color +body[data-theme="dracula"] + ::-webkit-scrollbar-thumb + background-color rgba(0, 0, 0, 0.3) + .ModalBase + .modalBack + background-color $ui-dracula-backgroundColor + .sortableItemHelper + color: $ui-dracula-text-color + body[data-theme="default"] .SideNav ::-webkit-scrollbar-thumb background-color rgba(255, 255, 255, 0.3) diff --git a/browser/main/lib/AwsMobileAnalyticsConfig.js b/browser/main/lib/AwsMobileAnalyticsConfig.js index 1ef4f8da..e4a21a92 100644 --- a/browser/main/lib/AwsMobileAnalyticsConfig.js +++ b/browser/main/lib/AwsMobileAnalyticsConfig.js @@ -45,7 +45,6 @@ function initAwsMobileAnalytics () { if (getSendEventCond()) return AWS.config.credentials.get((err) => { if (!err) { - console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId) recordDynamicCustomEvent('APP_STARTED') recordStaticCustomEvent() } @@ -58,7 +57,7 @@ function recordDynamicCustomEvent (type, options = {}) { mobileAnalyticsClient.recordEvent(type, options) } catch (analyticsError) { if (analyticsError instanceof ReferenceError) { - console.log(analyticsError.name + ': ' + analyticsError.message) + console.error(analyticsError.name + ': ' + analyticsError.message) } } } @@ -71,7 +70,7 @@ function recordStaticCustomEvent () { }) } catch (analyticsError) { if (analyticsError instanceof ReferenceError) { - console.log(analyticsError.name + ': ' + analyticsError.message) + console.error(analyticsError.name + ': ' + analyticsError.message) } } } diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 5ffb1bc7..c2ff9f7a 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -24,7 +24,9 @@ export const DEFAULT_CONFIG = { amaEnabled: true, hotkey: { toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E', - toggleMode: OSX ? 'Command + Option + M' : 'Ctrl + M' + toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', + deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace', + pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V' }, ui: { language: 'en', @@ -43,11 +45,16 @@ export const DEFAULT_CONFIG = { enableRulers: false, rulers: [80, 120], displayLineNumbers: true, - switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR + switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK' + delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE' scrollPastEnd: false, - type: 'SPLIT', + type: 'SPLIT', // 'SPLIT', 'EDITOR_PREVIEW' fetchUrlTitle: true, - enableTableEditor: false + enableTableEditor: false, + enableFrontMatterTitle: true, + frontMatterTitleField: 'title', + spellcheck: false, + enableSmartPaste: false }, preview: { fontSize: '14', @@ -60,12 +67,14 @@ export const DEFAULT_CONFIG = { latexBlockClose: '$$', plantUMLServerAddress: 'http://www.plantuml.com/plantuml', scrollPastEnd: false, + scrollSync: true, smartQuotes: true, breaks: true, smartArrows: false, allowCustomCSS: false, customCSS: '', - sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE' + sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE' + lineThroughCheckbox: true }, blog: { type: 'wordpress', // Available value: wordpress, add more types in the future plz @@ -147,6 +156,8 @@ function set (updates) { document.body.setAttribute('data-theme', 'solarized-dark') } else if (newConfig.ui.theme === 'monokai') { document.body.setAttribute('data-theme', 'monokai') + } else if (newConfig.ui.theme === 'dracula') { + document.body.setAttribute('data-theme', 'dracula') } else { document.body.setAttribute('data-theme', 'default') } @@ -195,7 +206,7 @@ function rewriteHotkey (config) { const keys = [...Object.keys(config.hotkey)] keys.forEach(key => { config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command') - config.hotkey[key] = config.hotkey[key].replace(/Opt/g, 'Alt') + config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ') }) return config } diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index 912450c1..373efddc 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -316,6 +316,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem reader.readAsDataURL(blob) } +/** + * @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code + * @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code + * @param {String} storageKey Key of the current storage + * @param {String} noteKey Key of the current note + * @param {NativeImage} image The native image + */ +function handlePastNativeImage (codeEditor, storageKey, noteKey, image) { + if (!codeEditor) { + throw new Error('codeEditor has to be given') + } + if (!storageKey) { + throw new Error('storageKey has to be given') + } + + if (!noteKey) { + throw new Error('noteKey has to be given') + } + if (!image) { + throw new Error('image has to be given') + } + + const targetStorage = findStorage.findStorage(storageKey) + const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + + createAttachmentDestinationFolder(targetStorage.path, noteKey) + + const imageName = `${uniqueSlug()}.png` + const imagePath = path.join(destinationDir, imageName) + + const binaryData = image.toPNG() + fs.writeFileSync(imagePath, binaryData, 'binary') + + const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName) + const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true) + codeEditor.insertAttachmentMd(imageMd) +} + /** * @description Returns all attachment paths of the given markdown * @param {String} markdownContent content in which the attachment paths should be found @@ -529,7 +567,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) { return modifiedLinkText }) } else { - console.log('One if the parameters was null -> Do nothing..') return Promise.resolve(linkText) } } @@ -540,6 +577,7 @@ module.exports = { generateAttachmentMarkdown, handleAttachmentDrop, handlePastImageEvent, + handlePastNativeImage, getAttachmentsInMarkdownContent, getAbsolutePathsOfAttachmentsInContent, removeStorageAndNoteReferences, diff --git a/browser/main/lib/dataApi/copyFile.js b/browser/main/lib/dataApi/copyFile.js index 2dc66309..6f23aae2 100755 --- a/browser/main/lib/dataApi/copyFile.js +++ b/browser/main/lib/dataApi/copyFile.js @@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) { const dstFolder = path.dirname(dstPath) if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder) - const input = fs.createReadStream(srcPath) + const input = fs.createReadStream(decodeURI(srcPath)) const output = fs.createWriteStream(dstPath) output.on('error', reject) diff --git a/browser/main/lib/dataApi/exportFolder.js b/browser/main/lib/dataApi/exportFolder.js index 3e998f15..771f77dc 100644 --- a/browser/main/lib/dataApi/exportFolder.js +++ b/browser/main/lib/dataApi/exportFolder.js @@ -1,9 +1,9 @@ import { findStorage } from 'browser/lib/findStorage' import resolveStorageData from './resolveStorageData' import resolveStorageNotes from './resolveStorageNotes' +import exportNote from './exportNote' import filenamify from 'filenamify' import * as path from 'path' -import * as fs from 'fs' /** * @param {String} storageKey @@ -45,9 +45,9 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) { notes .filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE') - .forEach(snippet => { - const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`) - fs.writeFileSync(notePath, snippet.content) + .forEach(note => { + const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`) + exportNote(note.key, storage.path, note.content, notePath, null) }) return { diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index e4fec5f4..b358e548 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -4,27 +4,43 @@ import { findStorage } from 'browser/lib/findStorage' const fs = require('fs') const path = require('path') +const attachmentManagement = require('./attachmentManagement') + /** - * Export note together with images + * Export note together with attachments * - * If images is stored in the storage, creates 'images' subfolder in target directory - * and copies images to it. Changes links to images in the content of the note + * If attachments are stored in the storage, creates 'attachments' subfolder in target directory + * and copies attachments to it. Changes links to images in the content of the note * + * @param {String} nodeKey key of the node that should be exported * @param {String} storageKey or storage path * @param {String} noteContent Content to export * @param {String} targetPath Path to exported file * @param {function} outputFormatter * @return {Promise.<*[]>} */ -function exportNote (storageKey, noteContent, targetPath, outputFormatter) { +function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) { const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path const exportTasks = [] if (!storagePath) { throw new Error('Storage path is not found') } + const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( + noteContent, + storagePath + ) + attachmentsAbsolutePaths.forEach(attachment => { + exportTasks.push({ + src: attachment, + dst: attachmentManagement.DESTINATION_FOLDER + }) + }) - let exportedData = noteContent + let exportedData = attachmentManagement.removeStorageAndNoteReferences( + noteContent, + nodeKey + ) if (outputFormatter) { exportedData = outputFormatter(exportedData, exportTasks) diff --git a/browser/main/lib/dataApi/renameStorage.js b/browser/main/lib/dataApi/renameStorage.js index 78242bed..3b806d1c 100644 --- a/browser/main/lib/dataApi/renameStorage.js +++ b/browser/main/lib/dataApi/renameStorage.js @@ -14,7 +14,6 @@ function renameStorage (key, name) { cachedStorageList = JSON.parse(localStorage.getItem('storages')) if (!_.isArray(cachedStorageList)) throw new Error('invalid storages') } catch (err) { - console.log('error got') console.error(err) return Promise.reject(err) } diff --git a/browser/main/lib/dataApi/resolveStorageData.js b/browser/main/lib/dataApi/resolveStorageData.js index 681a102e..da41f3d0 100644 --- a/browser/main/lib/dataApi/resolveStorageData.js +++ b/browser/main/lib/dataApi/resolveStorageData.js @@ -31,13 +31,9 @@ function resolveStorageData (storageCache) { const version = parseInt(storage.version, 10) if (version >= 1) { - if (version > 1) { - console.log('The repository version is newer than one of current app.') - } return Promise.resolve(storage) } - console.log('Transform Legacy storage', storage.path) return migrateFromV6Storage(storage.path) .then(() => storage) } diff --git a/browser/main/lib/dataApi/resolveStorageNotes.js b/browser/main/lib/dataApi/resolveStorageNotes.js index fa3f19ae..9da27248 100644 --- a/browser/main/lib/dataApi/resolveStorageNotes.js +++ b/browser/main/lib/dataApi/resolveStorageNotes.js @@ -9,7 +9,7 @@ function resolveStorageNotes (storage) { notePathList = sander.readdirSync(notesDirPath) } catch (err) { if (err.code === 'ENOENT') { - console.log(notesDirPath, ' doesn\'t exist.') + console.error(notesDirPath, ' doesn\'t exist.') sander.mkdirSync(notesDirPath) } else { console.warn('Failed to find note dir', notesDirPath, err) diff --git a/browser/main/lib/dataApi/toggleStorage.js b/browser/main/lib/dataApi/toggleStorage.js index dbb625c3..246d85ef 100644 --- a/browser/main/lib/dataApi/toggleStorage.js +++ b/browser/main/lib/dataApi/toggleStorage.js @@ -12,7 +12,6 @@ function toggleStorage (key, isOpen) { cachedStorageList = JSON.parse(localStorage.getItem('storages')) if (!_.isArray(cachedStorageList)) throw new Error('invalid storages') } catch (err) { - console.log('error got') console.error(err) return Promise.reject(err) } diff --git a/browser/main/lib/eventEmitter.js b/browser/main/lib/eventEmitter.js index de08f078..1276545b 100644 --- a/browser/main/lib/eventEmitter.js +++ b/browser/main/lib/eventEmitter.js @@ -14,7 +14,6 @@ function once (name, listener) { } function emit (name, ...args) { - console.log(name) remote.getCurrentWindow().webContents.send(name, ...args) } diff --git a/browser/main/lib/ipcClient.js b/browser/main/lib/ipcClient.js index 0c916617..c06296b5 100644 --- a/browser/main/lib/ipcClient.js +++ b/browser/main/lib/ipcClient.js @@ -14,14 +14,13 @@ nodeIpc.connectTo( path.join(app.getPath('userData'), 'boostnote.service'), function () { nodeIpc.of.node.on('error', function (err) { - console.log(err) + console.error(err) }) nodeIpc.of.node.on('connect', function () { - console.log('Connected successfully') ipcRenderer.send('config-renew', {config: ConfigManager.get()}) }) nodeIpc.of.node.on('disconnect', function () { - console.log('disconnected') + return }) } ) diff --git a/browser/main/lib/shortcut.js b/browser/main/lib/shortcut.js index a6f33196..93e33c9b 100644 --- a/browser/main/lib/shortcut.js +++ b/browser/main/lib/shortcut.js @@ -3,5 +3,8 @@ import ee from 'browser/main/lib/eventEmitter' module.exports = { 'toggleMode': () => { ee.emit('topbar:togglemodebutton') + }, + 'deleteNote': () => { + ee.emit('hotkey:deletenote') } } diff --git a/browser/main/modals/CreateFolderModal.styl b/browser/main/modals/CreateFolderModal.styl index 1b96e123..93848683 100644 --- a/browser/main/modals/CreateFolderModal.styl +++ b/browser/main/modals/CreateFolderModal.styl @@ -128,3 +128,29 @@ body[data-theme="monokai"] .control-confirmButton colorMonokaiPrimaryButton() + +body[data-theme="dracula"] + .root + modalDracula() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-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() \ No newline at end of file diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index 8b16f2a2..a190602c 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -21,8 +21,8 @@ class NewNoteModal extends React.Component { } handleMarkdownNoteButtonClick (e) { - const { storage, folder, dispatch, location } = this.props - createMarkdownNote(storage, folder, dispatch, location).then(() => { + const { storage, folder, dispatch, location, params, config } = this.props + createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => { setTimeout(this.props.close, 200) }) } @@ -35,8 +35,8 @@ class NewNoteModal extends React.Component { } handleSnippetNoteButtonClick (e) { - const { storage, folder, dispatch, location, config } = this.props - createSnippetNote(storage, folder, dispatch, location, config).then(() => { + const { storage, folder, dispatch, location, params, config } = this.props + createSnippetNote(storage, folder, dispatch, location, params, config).then(() => { setTimeout(this.props.close, 200) }) } diff --git a/browser/main/modals/NewNoteModal.styl b/browser/main/modals/NewNoteModal.styl index db14133f..c82b9376 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -97,3 +97,20 @@ body[data-theme="monokai"] .description color $ui-monokai-text-color + +body[data-theme="dracula"] + .root + background-color transparent + + .header + color $ui-dracula-text-color + + .control-button + border-color $ui-dracula-borderColor + color $ui-dracula-text-color + background-color transparent + &:focus + colorDraculaPrimaryButton() + + .description + color $ui-dracula-text-color \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/ConfigTab.styl b/browser/main/modals/PreferencesModal/ConfigTab.styl index b146486d..255165ce 100644 --- a/browser/main/modals/PreferencesModal/ConfigTab.styl +++ b/browser/main/modals/PreferencesModal/ConfigTab.styl @@ -138,6 +138,10 @@ colorMonokaiControl() background-color $ui-monokai-button-backgroundColor color $ui-monokai-text-color +colorDraculaControl() + border none + background-color $ui-dracula-button-backgroundColor + color $ui-dracula-text-color body[data-theme="dark"] .root @@ -220,3 +224,30 @@ body[data-theme="monokai"] .group-section-control select, .group-section-control-input colorMonokaiControl() + +body[data-theme="dracula"] + .root + color $ui-dracula-text-color + + .group-header + color $ui-dracula-text-color + border-color $ui-dracula-borderColor + + .group-header2 + color $ui-dracula-text-color + + .group-section-control-input + border-color $ui-dracula-borderColor + + .group-control + border-color $ui-dracula-borderColor + .group-control-leftButton + colorDarkDefaultButton() + border-color $ui-dracula-borderColor + .group-control-rightButton + colorDraculaPrimaryButton() + .group-hint + colorDraculaControl() + .group-section-control + select, .group-section-control-input + colorDraculaControl() \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.js b/browser/main/modals/PreferencesModal/Crowdfunding.js index f342fb76..f6389cd8 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.js +++ b/browser/main/modals/PreferencesModal/Crowdfunding.js @@ -23,21 +23,29 @@ class Crowdfunding extends React.Component { return (
{i18n.__('Crowdfunding')}
-

{i18n.__('Dear Boostnote users,')}

-

{i18n.__('Thank you for using Boostnote!')}

-

{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}


-

{i18n.__('To support our growing userbase, and satisfy community expectations,')}

-

{i18n.__('we would like to invest more time and resources in this project.')}

+

{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}

+

{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}


-

{i18n.__('If you use Boostnote and see its potential, help us out by supporting the project on OpenCollective!')}

+

{i18n.__('### Sustainable Open Source Ecosystem')}

+

{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}

+

{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}

+

{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}


-

{i18n.__('Thanks,')}

+

{i18n.__('### We believe Meritocracy')}

+

{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}

+

{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}

+

{i18n.__('It sometimes looks like exploitation.')}

+

{i18n.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}

+
+

{i18n.__('As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.')}

+
+

{i18n.__('Thank you,')}

{i18n.__('The Boostnote Team')}


) diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.styl b/browser/main/modals/PreferencesModal/Crowdfunding.styl index 326867d3..6d72290b 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.styl +++ b/browser/main/modals/PreferencesModal/Crowdfunding.styl @@ -29,7 +29,7 @@ p body[data-theme="dark"] p color $ui-dark-text-color - + body[data-theme="solarized-dark"] .root color $ui-solarized-dark-text-color @@ -41,3 +41,9 @@ body[data-theme="monokai"] color $ui-monokai-text-color p color $ui-monokai-text-color + +body[data-theme="dracula"] + .root + color $ui-dracula-text-color + p + color $ui-dracula-text-color \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/FolderItem.styl b/browser/main/modals/PreferencesModal/FolderItem.styl index f4a44675..2ded3ada 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.styl +++ b/browser/main/modals/PreferencesModal/FolderItem.styl @@ -154,3 +154,26 @@ body[data-theme="monokai"] .folderItem-right-dangerButton colorMonokaiPrimaryButton() + +body[data-theme="dracula"] + .folderItem + &:hover + background-color $ui-dracula-button-backgroundColor + + .folderItem-left-danger + color $danger-color + + .folderItem-left-key + color $ui-dark-inactive-text-color + + .folderItem-left-colorButton + colorDraculaPrimaryButton() + + .folderItem-right-button + colorDraculaPrimaryButton() + + .folderItem-right-confirmButton + colorDraculaPrimaryButton() + + .folderItem-right-dangerButton + colorDraculaPrimaryButton() \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 1c40a13a..a0f6a739 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -28,10 +28,20 @@ class HotkeyTab extends React.Component { }}) } this.handleSettingError = (err) => { - this.setState({keymapAlert: { - type: 'error', - message: err.message != null ? err.message : i18n.__('An error occurred!') - }}) + if ( + this.state.config.hotkey.toggleMain === '' || + this.state.config.hotkey.toggleMode === '' + ) { + this.setState({keymapAlert: { + type: 'success', + message: i18n.__('Successfully applied!') + }}) + } else { + this.setState({keymapAlert: { + type: 'error', + message: err.message != null ? err.message : i18n.__('An error occurred!') + }}) + } } this.oldHotkey = this.state.config.hotkey ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) @@ -68,7 +78,9 @@ class HotkeyTab extends React.Component { const { config } = this.state config.hotkey = { toggleMain: this.refs.toggleMain.value, - toggleMode: this.refs.toggleMode.value + toggleMode: this.refs.toggleMode.value, + deleteNote: this.refs.deleteNote.value, + pasteSmartly: this.refs.pasteSmartly.value } this.setState({ config @@ -127,6 +139,28 @@ class HotkeyTab extends React.Component { />
+
+
{i18n.__('Delete Note')}
+
+ this.handleHotkeyChange(e)} + ref='deleteNote' + value={config.hotkey.deleteNote} + type='text' + /> +
+
+
+
{i18n.__('Paste Smartly')}
+
+ this.handleHotkeyChange(e)} + ref='pasteSmartly' + value={config.hotkey.pasteSmartly} + type='text' + /> +
+
+
+
{i18n.__('Snippet name')}
diff --git a/browser/main/modals/PreferencesModal/SnippetTab.styl b/browser/main/modals/PreferencesModal/SnippetTab.styl index 02307b64..dd22b72e 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.styl +++ b/browser/main/modals/PreferencesModal/SnippetTab.styl @@ -196,3 +196,19 @@ body[data-theme="monokai"] color white .group-control-button colorMonokaiPrimaryButton() + +body[data-theme="dracula"] + .snippets + background $ui-dracula-backgroundColor + .snippet-item + color #f8f8f2 + &::after + background $ui-dracula-borderColor + &:hover + background darken($ui-dracula-backgroundColor, 5) + .snippet-item-selected + background darken($ui-dracula-backgroundColor, 5) + .snippet-detail + color #f8f8f2 + .group-control-button + colorDraculaPrimaryButton() \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/StoragesTab.styl b/browser/main/modals/PreferencesModal/StoragesTab.styl index 9804d7e7..9a1a0ef8 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.styl +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -158,7 +158,7 @@ body[data-theme="dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-dark-borderColor - + body[data-theme="solarized-dark"] @@ -236,3 +236,41 @@ body[data-theme="monokai"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-monokai-borderColor + +body[data-theme="dracula"] + .root + color $ui-dracula-text-color + + .folderList-item + border-bottom $ui-dracula-borderColor + + .folderList-empty + color $ui-dracula-text-color + + .list-empty + color $ui-dracula-text-color + .list-control-addStorageButton + border-color $ui-dracula-button-backgroundColor + background-color $ui-dracula-button-backgroundColor + color $ui-dracula-text-color + + .addStorage-header + color $ui-dracula-text-color + border-color $ui-dracula-borderColor + + .addStorage-body-section-name-input + border-color $$ui-dracula-borderColor + + .addStorage-body-section-type-description + color $ui-dracula-text-color + + .addStorage-body-section-path-button + colorPrimaryButton() + .addStorage-body-control + border-color $ui-dracula-borderColor + + .addStorage-body-control-createButton + colorDarkPrimaryButton() + .addStorage-body-control-cancelButton + colorDarkDefaultButton() + border-color $ui-dracula-borderColor \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index a45e1387..becd4f54 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -68,9 +68,13 @@ class UiTab extends React.Component { theme: this.refs.uiTheme.value, language: this.refs.uiLanguage.value, defaultNote: this.refs.defaultNote.value, + tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags.checked, showCopyNotification: this.refs.showCopyNotification.checked, confirmDeletion: this.refs.confirmDeletion.checked, showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked, + showTagsAlphabetically: this.refs.showTagsAlphabetically.checked, + saveTagsAlphabetically: this.refs.saveTagsAlphabetically.checked, + enableLiveNoteCounts: this.refs.enableLiveNoteCounts.checked, disableDirectWrite: this.refs.uiD2w != null ? this.refs.uiD2w.checked : false @@ -89,7 +93,11 @@ class UiTab extends React.Component { snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value, scrollPastEnd: this.refs.scrollPastEnd.checked, fetchUrlTitle: this.refs.editorFetchUrlTitle.checked, - enableTableEditor: this.refs.enableTableEditor.checked + enableTableEditor: this.refs.enableTableEditor.checked, + enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked, + frontMatterTitleField: this.refs.frontMatterTitleField.value, + spellcheck: this.refs.spellcheck.checked, + enableSmartPaste: this.refs.enableSmartPaste.checked }, preview: { fontSize: this.refs.previewFontSize.value, @@ -102,11 +110,13 @@ class UiTab extends React.Component { latexBlockClose: this.refs.previewLatexBlockClose.value, plantUMLServerAddress: this.refs.previewPlantUMLServerAddress.value, scrollPastEnd: this.refs.previewScrollPastEnd.checked, + scrollSync: this.refs.previewScrollSync.checked, smartQuotes: this.refs.previewSmartQuotes.checked, breaks: this.refs.previewBreaks.checked, smartArrows: this.refs.previewSmartArrows.checked, sanitize: this.refs.previewSanitize.value, allowCustomCSS: this.refs.previewAllowCustomCSS.checked, + lineThroughCheckbox: this.refs.lineThroughCheckbox.checked, customCSS: this.customCSSCM.getCodeMirror().getValue() } } @@ -187,6 +197,7 @@ class UiTab extends React.Component { +
@@ -244,16 +255,6 @@ class UiTab extends React.Component { {i18n.__('Show a confirmation dialog when deleting notes')}
-
- -
{ global.process.platform === 'win32' ?
@@ -269,6 +270,64 @@ class UiTab extends React.Component {
: null } + +
Tags
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
Editor
@@ -426,6 +485,31 @@ class UiTab extends React.Component {
+
+
+ {i18n.__('Front matter title field')} +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+ +
+ +
+
+
+ +
+ +
+ +
+
{i18n.__('Preview')}
@@ -512,6 +618,16 @@ class UiTab extends React.Component {
+
+ +
+
+ +