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..130cc86e 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -11,9 +11,14 @@ 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 } = 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' @@ -28,7 +33,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) } @@ -57,9 +62,18 @@ export default class CodeEditor extends React.Component { } this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchState = null + this.scrollToLineHandeler = this.scrollToLine.bind(this) this.formatTable = () => this.handleFormatTable() + 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) { @@ -125,6 +139,7 @@ export default class CodeEditor extends React.Component { componentDidMount () { const { rulers, enableRulers } = this.props const expandSnippet = this.expandSnippet.bind(this) + eventEmitter.on('line:jump', this.scrollToLineHandeler) const defaultSnippet = [ { @@ -226,6 +241,7 @@ 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) + this.editor.on('contextmenu', this.contextMenuHandler) eventEmitter.on('top:search', this.searchHandler) eventEmitter.emit('code:init') @@ -242,6 +258,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 +331,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 +409,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) } @@ -453,6 +481,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 +504,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() @@ -538,7 +583,11 @@ export default class CodeEditor extends React.Component { ) return prevChar === '](' && nextChar === ')' } - if (dataTransferItem.type.match('image')) { + + const pastedHtml = clipboardData.getData('text/html') + if (pastedHtml !== '') { + this.handlePasteHtml(e, editor, pastedHtml) + } else if (dataTransferItem.type.match('image')) { attachmentManagement.handlePastImageEvent( this, storageKey, @@ -608,6 +657,12 @@ export default class CodeEditor extends React.Component { }) } + handlePasteHtml (e, editor, pastedHtml) { + e.preventDefault() + const markdown = this.turndownService.turndown(pastedHtml) + editor.replaceSelection(markdown) + } + mapNormalResponse (response, pastedTxt) { return this.decodeResponse(response).then(body => { return new Promise((resolve, reject) => { @@ -690,6 +745,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 +774,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 +785,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..20ce9451 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,7 @@ class MarkdownEditor extends React.Component { enableTableEditor={config.editor.enableTableEditor} onChange={(e) => this.handleChange(e)} onBlur={(e) => this.handleBlur(e)} + spellCheck={config.editor.spellcheck} /> ) 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..463f0a16 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': @@ -325,9 +337,7 @@ 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, @@ -425,6 +435,7 @@ export default class MarkdownPreview extends React.Component { case 'dark': case 'solarized-dark': case 'monokai': + case 'dracula': return scrollBarDarkStyle default: return scrollBarStyle @@ -526,7 +537,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 +744,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 +764,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 +774,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 +872,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..0ab52a56 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,7 @@ class MarkdownSplitEditor extends React.Component { noteKey={noteKey} onChange={this.handleOnChange.bind(this)} onScroll={this.handleScroll.bind(this)} + spellCheck={config.editor.spellcheck} />
this.handleMouseDown(e)} >
@@ -192,6 +201,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.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/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..4a38ffe5 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() } @@ -759,6 +768,8 @@ class SnippetNoteDetail extends React.Component { this.handleChange(e)} /> 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 30ad93c3..7bb52ccd 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -56,7 +56,6 @@ class NoteList extends React.Component { super(props) this.selectNextNoteHandler = () => { - console.log('fired next') this.selectNextNote() } this.selectPriorNoteHandler = () => { @@ -84,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: [] } @@ -267,7 +268,7 @@ class NoteList extends React.Component { } handleNoteListKeyDown (e) { - if (e.metaKey || e.ctrlKey) return true + if (e.metaKey) return true // A key if (e.keyCode === 65 && !e.shiftKey) { @@ -307,6 +308,8 @@ class NoteList extends React.Component { if (e.shiftKey) { this.setState({ shiftKeyDown: true }) + } else if (e.ctrlKey) { + this.setState({ ctrlKeyDown: true }) } } @@ -314,6 +317,10 @@ class NoteList extends React.Component { if (!e.shiftKey) { this.setState({ shiftKeyDown: false }) } + + if (!e.ctrlKey) { + this.setState({ ctrlKeyDown: false }) + } } getNotes () { @@ -390,25 +397,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({ @@ -616,7 +663,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 @@ -636,7 +682,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) @@ -996,6 +1041,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..7b5caf72 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -274,7 +274,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) 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..d6b04d9b 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -24,7 +24,8 @@ 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' }, ui: { language: 'en', @@ -43,11 +44,15 @@ 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 }, preview: { fontSize: '14', @@ -60,12 +65,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 +154,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 +204,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..c193eaf2 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -529,7 +529,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) { return modifiedLinkText }) } else { - console.log('One if the parameters was null -> Do nothing..') return Promise.resolve(linkText) } } 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..7ad6f606 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,8 @@ 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 } this.setState({ config @@ -127,6 +138,17 @@ class HotkeyTab extends React.Component { />
+
+
{i18n.__('Delete Note')}
+
+ this.handleHotkeyChange(e)} + ref='deleteNote' + value={config.hotkey.deleteNote} + 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..a52bee9d 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,10 @@ 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 }, preview: { fontSize: this.refs.previewFontSize.value, @@ -102,11 +109,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 +196,7 @@ class UiTab extends React.Component { +
@@ -244,16 +254,6 @@ class UiTab extends React.Component { {i18n.__('Show a confirmation dialog when deleting notes')}
-
- -
{ global.process.platform === 'win32' ?
@@ -269,6 +269,64 @@ class UiTab extends React.Component {
: null } + +
Tags
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
Editor
@@ -426,6 +484,31 @@ class UiTab extends React.Component {
+
+
+ {i18n.__('Front matter title field')} +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+ +
+ +
+
+
+ +
{i18n.__('Preview')}
@@ -512,6 +605,16 @@ class UiTab extends React.Component {
+
+ +
+
+ +