diff --git a/.editorconfig b/.editorconfig index a4730cbf..8c5bd614 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# EditorConfig is awesome: http://EditorConfig.org +# EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..9d1cc4ec --- /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_modules/.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/Backers.md b/Backers.md deleted file mode 100644 index 18d221bf..00000000 --- a/Backers.md +++ /dev/null @@ -1,72 +0,0 @@ -

Sponsors & Backers

- -Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome backers. If you'd like to join them, please consider: - -- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio) - ---- - -## Backers via OpenCollective - -### [Gold Sponsors / $1,000 per month](https://opencollective.com/boostnoteio/order/2259) -- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/. - -### [Silver Sponsors / $250 per month](https://opencollective.com/boostnoteio/order/2257) -- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/. - -### [Bronze Sponsors / $50 per month](https://opencollective.com/boostnoteio/order/2258) -- Get your name and Url (or E-mail) on Readme.md on GitHub. - -### [Backers3 / $10 per month](https://opencollective.com/boostnoteio/order/2176) -- [Ralph03](https://opencollective.com/ralph03) - -- [Nikolas Dan](https://opencollective.com/nikolas-dan) - -### [Backers2 / $5 per month](https://opencollective.com/boostnoteio/order/2175) -- [Yeojong Kim](https://twitter.com/yeojoy) - -- [Scotia Draven](https://opencollective.com/scotia-draven) - -- [A. J. Vargas](https://opencollective.com/aj-vargas) - -### [Backers1](https://opencollective.com/boostnoteio/order/2563) and One-time sponsors -- Ryosuke Tamura - $30 - -- tatoosh11 - $10 - -- Alexander Borovkov - $10 - -- spoonhoop - $5 - -- Drew Williams - $2 - -- Andy Shaw - $2 - -- mysafesky -$2 - ---- - -## Backers via Bountysource -https://salt.bountysource.com/teams/boostnote - -- Kuzz - $65 - -- Intense Raiden - $45 - -- ravy22 - $25 - -- trentpolack - $20 - -- hikariru - $10 - -- kolchan11 - $10 - -- RonWalker22 - $10 - -- hocchuc - $5 - -- Adam - $5 - -- Steve - $5 - -- evmin - $5 diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 00000000..c7fc4016 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,29 @@ +# Frequently Asked Questions + + +
Allowing dangerous HTML tags + +Sometimes it is useful to allow dangerous HTML tags to add interactivity to your notebook. One of the example is to use details/summary as a way to expand/collaps your todo-list. + +* How to enable: + * Go to **Preferences** → **Interface** → **Sanitization** → **Allow dangerous html tags** +* Example note: Multiple todo-list + * Create new notes + * Paste the below code, and you'll see that you can expand/collaps the todo-list, and you can have multiple todo-list in your note. + +```html +
What I want to do + +- [x] Create an awesome feature X +- [ ] Do my homework + +
+``` + +
+ +## Other questions + +You can ask [here][ISSUES] + +[ISSUES]: https://github.com/BoostIO/Boostnote/issues diff --git a/LICENSE b/LICENSE index 7472c9eb..2d1ab131 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ GPL-3.0 Boostnote - an open source note-taking app made for programmers just like you. -Copyright (C) 2017 - 2018 BoostIO +Copyright (C) 2017 - 2019 BoostIO This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by 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..48634993 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -2,23 +2,120 @@ import PropTypes from 'prop-types' import React from 'react' import _ from 'lodash' import CodeMirror from 'codemirror' +import hljs from 'highlight.js' import 'codemirror-mode-elixir' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import convertModeName from 'browser/lib/convertModeName' -import { options, TableEditor, Alignment } from '@susisu/mte-kernel' +import { + options, + TableEditor, + Alignment +} from '@susisu/mte-kernel' import TextEditorInterface from 'browser/lib/TextEditorInterface' import eventEmitter from 'browser/main/lib/eventEmitter' import iconv from 'iconv-lite' import crypto from 'crypto' import consts from 'browser/lib/consts' +import styles from '../components/CodeEditor.styl' import fs from 'fs' -const { ipcRenderer } = require('electron') +const { ipcRenderer, remote, clipboard } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' +const spellcheck = require('browser/lib/spellcheck') +const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') +import TurndownService from 'turndown' +import { + gfm +} from 'turndown-plugin-gfm' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' const buildCMRulers = (rulers, enableRulers) => - (enableRulers ? rulers.map(ruler => ({ column: ruler })) : []) + (enableRulers ? rulers.map(ruler => ({ + column: ruler + })) : []) + +function translateHotkey (hotkey) { + return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl') +} + +const languageMaps = { + brainfuck: 'Brainfuck', + cpp: 'C++', + cs: 'C#', + clojure: 'Clojure', + 'clojure-repl': 'ClojureScript', + cmake: 'CMake', + coffeescript: 'CoffeeScript', + crystal: 'Crystal', + css: 'CSS', + d: 'D', + dart: 'Dart', + delphi: 'Pascal', + diff: 'Diff', + django: 'Django', + dockerfile: 'Dockerfile', + ebnf: 'EBNF', + elm: 'Elm', + erlang: 'Erlang', + 'erlang-repl': 'Erlang', + fortran: 'Fortran', + fsharp: 'F#', + gherkin: 'Gherkin', + go: 'Go', + groovy: 'Groovy', + haml: 'HAML', + haskell: 'Haskell', + haxe: 'Haxe', + http: 'HTTP', + ini: 'toml', + java: 'Java', + javascript: 'JavaScript', + json: 'JSON', + julia: 'Julia', + kotlin: 'Kotlin', + less: 'LESS', + livescript: 'LiveScript', + lua: 'Lua', + markdown: 'Markdown', + mathematica: 'Mathematica', + nginx: 'Nginx', + nsis: 'NSIS', + objectivec: 'Objective-C', + ocaml: 'Ocaml', + perl: 'Perl', + php: 'PHP', + powershell: 'PowerShell', + properties: 'Properties files', + protobuf: 'ProtoBuf', + python: 'Python', + puppet: 'Puppet', + q: 'Q', + r: 'R', + ruby: 'Ruby', + rust: 'Rust', + sas: 'SAS', + scala: 'Scala', + scheme: 'Scheme', + scss: 'SCSS', + shell: 'Shell', + smalltalk: 'Smalltalk', + sml: 'SML', + sql: 'SQL', + stylus: 'Stylus', + swift: 'Swift', + tcl: 'Tcl', + tex: 'LaTex', + typescript: 'TypeScript', + twig: 'Twig', + vbnet: 'VB.NET', + vbscript: 'VBScript', + verilog: 'Verilog', + vhdl: 'VHDL', + xml: 'HTML', + xquery: 'XQuery', + yaml: 'YAML', + elixir: 'Elixir' +} export default class CodeEditor extends React.Component { constructor (props) { @@ -28,7 +125,8 @@ 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.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject) this.focusHandler = () => { ipcRenderer.send('editor:focused', true) } @@ -44,22 +142,42 @@ export default class CodeEditor extends React.Component { } this.props.onBlur != null && this.props.onBlur(e) - const { storageKey, noteKey } = this.props + const { + storageKey, + noteKey + } = this.props attachmentManagement.deleteAttachmentsNotPresentInNote( this.editor.getValue(), storageKey, noteKey ) } - this.pasteHandler = (editor, e) => this.handlePaste(editor, e) + this.pasteHandler = (editor, e) => { + e.preventDefault() + + this.handlePaste(editor, false) + } this.loadStyleHandler = e => { this.editor.refresh() } this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchState = null + this.scrollToLineHandeler = this.scrollToLine.bind(this) this.formatTable = () => this.handleFormatTable() + + if (props.switchPreview !== 'RIGHTCLICK') { + this.contextMenuHandler = function (editor, event) { + const menu = buildEditorContextMenu(editor, event) + if (menu != null) { + setTimeout(() => menu.popup(remote.getCurrentWindow()), 30) + } + } + } + this.editorActivityHandler = () => this.handleEditorActivity() + + this.turndownService = new TurndownService() } handleSearch (msg) { @@ -97,7 +215,9 @@ export default class CodeEditor extends React.Component { } handleFormatTable () { - this.tableEditor.formatAll(options({textWidthOptions: {}})) + this.tableEditor.formatAll(options({ + textWidthOptions: {} + })) } handleEditorActivity () { @@ -106,42 +226,10 @@ export default class CodeEditor extends React.Component { } } - updateTableEditorState () { - const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) - if (active) { - if (this.extraKeysMode !== 'editor') { - this.extraKeysMode = 'editor' - this.editor.setOption('extraKeys', this.editorKeyMap) - } - } else { - if (this.extraKeysMode !== 'default') { - this.extraKeysMode = 'default' - this.editor.setOption('extraKeys', this.defaultKeyMap) - this.tableEditor.resetSmartCursor() - } - } - } - - componentDidMount () { - const { rulers, enableRulers } = this.props + updateDefaultKeyMap () { + const { hotkey } = this.props const expandSnippet = this.expandSnippet.bind(this) - const defaultSnippet = [ - { - id: crypto.randomBytes(16).toString('hex'), - name: 'Dummy text', - prefix: ['lorem', 'ipsum'], - content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' - } - ] - if (!fs.existsSync(consts.SNIPPET_FILE)) { - fs.writeFileSync( - consts.SNIPPET_FILE, - JSON.stringify(defaultSnippet, null, 4), - 'utf8' - ) - } - this.defaultKeyMap = CodeMirror.normalizeKeyMap({ Tab: function (cm) { const cursor = cm.getCursor() @@ -183,6 +271,9 @@ export default class CodeEditor extends React.Component { } } }, + 'Cmd-Left': function (cm) { + cm.execCommand('goLineLeft') + }, 'Cmd-T': function (cm) { // Do nothing }, @@ -192,13 +283,56 @@ export default class CodeEditor extends React.Component { document.execCommand('copy') } return CodeMirror.Pass + }, + [translateHotkey(hotkey.pasteSmartly)]: cm => { + this.handlePaste(cm, true) } }) + } + + updateTableEditorState () { + const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) + if (active) { + if (this.extraKeysMode !== 'editor') { + this.extraKeysMode = 'editor' + this.editor.setOption('extraKeys', this.editorKeyMap) + } + } else { + if (this.extraKeysMode !== 'default') { + this.extraKeysMode = 'default' + this.editor.setOption('extraKeys', this.defaultKeyMap) + this.tableEditor.resetSmartCursor() + } + } + } + + componentDidMount () { + const { rulers, enableRulers } = this.props + eventEmitter.on('line:jump', this.scrollToLineHandeler) + + const defaultSnippet = [ + { + id: crypto.randomBytes(16).toString('hex'), + name: 'Dummy text', + prefix: ['lorem', 'ipsum'], + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + } + ] + if (!fs.existsSync(consts.SNIPPET_FILE)) { + fs.writeFileSync( + consts.SNIPPET_FILE, + JSON.stringify(defaultSnippet, null, 4), + 'utf8' + ) + } + + this.updateDefaultKeyMap() this.value = this.props.value this.editor = CodeMirror(this.refs.root, { rulers: buildCMRulers(rulers, enableRulers), value: this.props.value, + linesHighlighted: this.props.linesHighlighted, lineNumbers: this.props.displayLineNumbers, lineWrapping: true, theme: this.props.theme, @@ -212,20 +346,28 @@ export default class CodeEditor extends React.Component { foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], autoCloseBrackets: { - pairs: '()[]{}\'\'""$$**``', - triples: '```"""\'\'\'', - explode: '[]{}``$$', + pairs: this.props.matchingPairs, + triples: this.props.matchingTriples, + explode: this.props.explodingPairs, override: true }, extraKeys: this.defaultKeyMap }) - this.setMode(this.props.mode) + if (!this.props.mode && this.props.value && this.props.autoDetect) { + this.autoDetectLanguage(this.props.value) + } else { + this.setMode(this.props.mode) + } this.editor.on('focus', this.focusHandler) this.editor.on('blur', this.blurHandler) this.editor.on('change', this.changeHandler) + this.editor.on('gutterClick', this.highlightHandler) this.editor.on('paste', this.pasteHandler) + if (this.props.switchPreview !== 'RIGHTCLICK') { + this.editor.on('contextmenu', this.contextMenuHandler) + } eventEmitter.on('top:search', this.searchHandler) eventEmitter.emit('code:init') @@ -242,6 +384,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({ @@ -249,43 +395,117 @@ export default class CodeEditor extends React.Component { }) this.editorKeyMap = CodeMirror.normalizeKeyMap({ - 'Tab': () => { this.tableEditor.nextCell(this.tableEditorOptions) }, - 'Shift-Tab': () => { this.tableEditor.previousCell(this.tableEditorOptions) }, - 'Enter': () => { this.tableEditor.nextRow(this.tableEditorOptions) }, - 'Ctrl-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) }, - 'Cmd-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) }, - 'Shift-Ctrl-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) }, - 'Shift-Cmd-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) }, - 'Shift-Ctrl-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) }, - 'Shift-Cmd-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) }, - 'Shift-Ctrl-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) }, - 'Shift-Cmd-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) }, - 'Shift-Ctrl-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) }, - 'Shift-Cmd-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) }, - 'Ctrl-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) }, - 'Cmd-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) }, - 'Ctrl-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) }, - 'Cmd-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) }, - 'Ctrl-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) }, - 'Cmd-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) }, - 'Ctrl-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) }, - 'Cmd-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) }, - 'Ctrl-K Ctrl-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) }, - 'Cmd-K Cmd-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) }, - 'Ctrl-L Ctrl-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) }, - 'Cmd-L Cmd-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) }, - 'Ctrl-K Ctrl-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) }, - 'Cmd-K Cmd-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) }, - 'Ctrl-L Ctrl-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) }, - 'Cmd-L Cmd-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) }, - 'Alt-Shift-Ctrl-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) }, - 'Alt-Shift-Cmd-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) }, - 'Alt-Shift-Ctrl-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) }, - 'Alt-Shift-Cmd-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) }, - 'Alt-Shift-Ctrl-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) }, - 'Alt-Shift-Cmd-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) }, - 'Alt-Shift-Ctrl-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) }, - 'Alt-Shift-Cmd-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) } + 'Tab': () => { + this.tableEditor.nextCell(this.tableEditorOptions) + }, + 'Shift-Tab': () => { + this.tableEditor.previousCell(this.tableEditorOptions) + }, + 'Enter': () => { + this.tableEditor.nextRow(this.tableEditorOptions) + }, + 'Ctrl-Enter': () => { + this.tableEditor.escape(this.tableEditorOptions) + }, + 'Cmd-Enter': () => { + this.tableEditor.escape(this.tableEditorOptions) + }, + 'Shift-Ctrl-Left': () => { + this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) + }, + 'Shift-Cmd-Left': () => { + this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) + }, + 'Shift-Ctrl-Right': () => { + this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) + }, + 'Shift-Cmd-Right': () => { + this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) + }, + 'Shift-Ctrl-Up': () => { + this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) + }, + 'Shift-Cmd-Up': () => { + this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) + }, + 'Shift-Ctrl-Down': () => { + this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) + }, + 'Shift-Cmd-Down': () => { + this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) + }, + 'Ctrl-Left': () => { + this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) + }, + 'Cmd-Left': () => { + this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) + }, + 'Ctrl-Right': () => { + this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) + }, + 'Cmd-Right': () => { + this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) + }, + 'Ctrl-Up': () => { + this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) + }, + 'Cmd-Up': () => { + this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) + }, + 'Ctrl-Down': () => { + this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) + }, + 'Cmd-Down': () => { + this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) + }, + 'Ctrl-K Ctrl-I': () => { + this.tableEditor.insertRow(this.tableEditorOptions) + }, + 'Cmd-K Cmd-I': () => { + this.tableEditor.insertRow(this.tableEditorOptions) + }, + 'Ctrl-L Ctrl-I': () => { + this.tableEditor.deleteRow(this.tableEditorOptions) + }, + 'Cmd-L Cmd-I': () => { + this.tableEditor.deleteRow(this.tableEditorOptions) + }, + 'Ctrl-K Ctrl-J': () => { + this.tableEditor.insertColumn(this.tableEditorOptions) + }, + 'Cmd-K Cmd-J': () => { + this.tableEditor.insertColumn(this.tableEditorOptions) + }, + 'Ctrl-L Ctrl-J': () => { + this.tableEditor.deleteColumn(this.tableEditorOptions) + }, + 'Cmd-L Cmd-J': () => { + this.tableEditor.deleteColumn(this.tableEditorOptions) + }, + 'Alt-Shift-Ctrl-Left': () => { + this.tableEditor.moveColumn(-1, this.tableEditorOptions) + }, + 'Alt-Shift-Cmd-Left': () => { + this.tableEditor.moveColumn(-1, this.tableEditorOptions) + }, + 'Alt-Shift-Ctrl-Right': () => { + this.tableEditor.moveColumn(1, this.tableEditorOptions) + }, + 'Alt-Shift-Cmd-Right': () => { + this.tableEditor.moveColumn(1, this.tableEditorOptions) + }, + 'Alt-Shift-Ctrl-Up': () => { + this.tableEditor.moveRow(-1, this.tableEditorOptions) + }, + 'Alt-Shift-Cmd-Up': () => { + this.tableEditor.moveRow(-1, this.tableEditorOptions) + }, + 'Alt-Shift-Ctrl-Down': () => { + this.tableEditor.moveRow(1, this.tableEditorOptions) + }, + 'Alt-Shift-Cmd-Down': () => { + this.tableEditor.moveRow(1, this.tableEditorOptions) + } }) if (this.props.enableTableEditor) { @@ -296,6 +516,8 @@ export default class CodeEditor extends React.Component { this.setState({ clientWidth: this.refs.root.clientWidth }) + + this.initialHighlighting() } expandSnippet (line, cursor, cm, snippets) { @@ -311,22 +533,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, @@ -366,8 +594,14 @@ export default class CodeEditor extends React.Component { return { text: wordBeforeCursor, range: { - from: { line: lineNumber, ch: originCursorPosition }, - to: { line: lineNumber, ch: cursorPosition } + from: { + line: lineNumber, + ch: originCursorPosition + }, + to: { + line: lineNumber, + ch: cursorPosition + } } } } @@ -383,15 +617,20 @@ 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) } componentDidUpdate (prevProps, prevState) { let needRefresh = false - const { rulers, enableRulers } = this.props + const { + rulers, + enableRulers + } = this.props if (prevProps.mode !== this.props.mode) { this.setMode(this.props.mode) } @@ -432,6 +671,18 @@ export default class CodeEditor extends React.Component { this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) } + if (prevProps.matchingPairs !== this.props.matchingPairs || + prevProps.matchingTriples !== this.props.matchingTriples || + prevProps.explodingPairs !== this.props.explodingPairs) { + const bracketObject = { + pairs: this.props.matchingPairs, + triples: this.props.matchingTriples, + explode: this.props.explodingPairs, + override: true + } + this.editor.setOption('autoCloseBrackets', bracketObject) + } + if (prevProps.enableTableEditor !== this.props.enableTableEditor) { if (this.props.enableTableEditor) { this.editor.on('cursorActivity', this.editorActivityHandler) @@ -445,6 +696,14 @@ export default class CodeEditor extends React.Component { this.editor.setOption('extraKeys', this.defaultKeyMap) } + if (prevProps.hotkey !== this.props.hotkey) { + this.updateDefaultKeyMap() + + if (this.extraKeysMode === 'default') { + this.editor.setOption('extraKeys', this.defaultKeyMap) + } + } + if (this.state.clientWidth !== this.refs.root.clientWidth) { this.setState({ clientWidth: this.refs.root.clientWidth @@ -453,29 +712,130 @@ 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) + const elem = document.getElementById('editor-bottom-panel') + elem.parentNode.removeChild(elem) + } else { + this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) + } + } + if (needRefresh) { this.editor.refresh() } } setMode (mode) { - let syntax = CodeMirror.findModeByName(convertModeName(mode)) + let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text')) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') this.editor.setOption('mode', syntax.mime) CodeMirror.autoLoadMode(this.editor, syntax.mode) } - handleChange (e) { - this.value = this.editor.getValue() + handleChange (editor, changeObject) { + spellcheck.handleChange(editor, changeObject) + + this.updateHighlight(editor, changeObject) + + this.value = editor.getValue() if (this.props.onChange) { - this.props.onChange(e) + this.props.onChange(editor) } } + incrementLines (start, linesAdded, linesRemoved, editor) { + let highlightedLines = editor.options.linesHighlighted + + const totalHighlightedLines = highlightedLines.length + + let offset = linesAdded - linesRemoved + + // Store new items to be added as we're changing the lines + let newLines = [] + + let i = totalHighlightedLines + + while (i--) { + const lineNumber = highlightedLines[i] + + // Interval that will need to be updated + // Between start and (start + offset) remove highlight + if (lineNumber >= start) { + highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1) + + // Lines that need to be relocated + if (lineNumber >= (start + linesRemoved)) { + newLines.push(lineNumber + offset) + } + } + } + + // Adding relocated lines + highlightedLines.push(...newLines) + + if (this.props.onChange) { + this.props.onChange(editor) + } + } + + handleHighlight (editor, changeObject) { + const lines = editor.options.linesHighlighted + + if (!lines.includes(changeObject)) { + lines.push(changeObject) + editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background') + } else { + lines.splice(lines.indexOf(changeObject), 1) + editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background') + } + if (this.props.onChange) { + this.props.onChange(editor) + } + } + + updateHighlight (editor, changeObject) { + const linesAdded = changeObject.text.length - 1 + const linesRemoved = changeObject.removed.length - 1 + + // If no lines added or removed return + if (linesAdded === 0 && linesRemoved === 0) { + return + } + + let start = changeObject.from.line + + switch (changeObject.origin) { + case '+insert", "undo': + start += 1 + break + + case 'paste': + case '+delete': + case '+input': + if (changeObject.to.ch !== 0 || changeObject.from.ch !== 0) { + start += 1 + } + break + + default: + return + } + + this.incrementLines(start, linesAdded, linesRemoved, editor) + } + moveCursorTo (row, col) {} - scrollToLine (num) {} + scrollToLine (event, num) { + const cursor = { + line: num, + ch: 1 + } + this.editor.setCursor(cursor) + } focus () { this.editor.focus() @@ -491,6 +851,7 @@ export default class CodeEditor extends React.Component { this.value = this.props.value this.editor.setValue(this.props.value) this.editor.clearHistory() + this.restartHighlighting() this.editor.on('change', this.changeHandler) this.editor.refresh() } @@ -503,7 +864,10 @@ export default class CodeEditor extends React.Component { handleDropImage (dropEvent) { dropEvent.preventDefault() - const { storageKey, noteKey } = this.props + const { + storageKey, + noteKey + } = this.props attachmentManagement.handleAttachmentDrop( this, storageKey, @@ -516,49 +880,110 @@ export default class CodeEditor extends React.Component { this.editor.replaceSelection(imageMd) } - handlePaste (editor, e) { - const clipboardData = e.clipboardData - const { storageKey, noteKey } = this.props - const dataTransferItem = clipboardData.items[0] - const pastedTxt = clipboardData.getData('text') + autoDetectLanguage (content) { + const res = hljs.highlightAuto(content, Object.keys(languageMaps)) + this.setMode(languageMaps[res.language]) + } + + handlePaste (editor, forceSmartPaste) { + const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props + const isURL = str => { const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ return matcher.test(str) } + const isInLinkTag = editor => { const startCursor = editor.getCursor('start') - const prevChar = editor.getRange( - { line: startCursor.line, ch: startCursor.ch - 2 }, - { line: startCursor.line, ch: startCursor.ch } - ) + const prevChar = editor.getRange({ + line: startCursor.line, + ch: startCursor.ch - 2 + }, { + line: startCursor.line, + ch: startCursor.ch + }) const endCursor = editor.getCursor('end') - const nextChar = editor.getRange( - { line: endCursor.line, ch: endCursor.ch }, - { line: endCursor.line, ch: endCursor.ch + 1 } - ) + const nextChar = editor.getRange({ + line: endCursor.line, + ch: endCursor.ch + }, { + line: endCursor.line, + ch: endCursor.ch + 1 + }) return prevChar === '](' && nextChar === ')' } - if (dataTransferItem.type.match('image')) { - attachmentManagement.handlePastImageEvent( - this, - storageKey, - noteKey, - dataTransferItem - ) - } else if ( - this.props.fetchUrlTitle && - isURL(pastedTxt) && - !isInLinkTag(editor) - ) { - this.handlePasteUrl(e, editor, pastedTxt) + + const isInFencedCodeBlock = editor => { + const cursor = editor.getCursor() + + let token = editor.getTokenAt(cursor) + if (token.state.fencedState) { + return true + } + + let line = line = cursor.line - 1 + while (line >= 0) { + token = editor.getTokenAt({ + ch: 3, + line + }) + + if (token.start === token.end) { + --line + } else if (token.type === 'comment') { + if (line > 0) { + token = editor.getTokenAt({ + ch: 3, + line: line - 1 + }) + + return token.type !== 'comment' + } else { + return true + } + } else { + return false + } + } + + return false } - if (attachmentManagement.isAttachmentLink(pastedTxt)) { + + const pastedTxt = clipboard.readText() + + if (isInFencedCodeBlock(editor)) { + this.handlePasteText(editor, pastedTxt) + } else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { + this.handlePasteUrl(editor, pastedTxt) + } else if (attachmentManagement.isAttachmentLink(pastedTxt)) { attachmentManagement .handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt) .then(modifiedText => { this.editor.replaceSelection(modifiedText) }) - e.preventDefault() + } else { + const image = clipboard.readImage() + if (!image.isEmpty()) { + attachmentManagement.handlePastNativeImage( + this, + storageKey, + noteKey, + image + ) + } else if (enableSmartPaste || forceSmartPaste) { + const pastedHtml = clipboard.readHTML() + if (pastedHtml.length > 0) { + this.handlePasteHtml(editor, pastedHtml) + } else { + this.handlePasteText(editor, pastedTxt) + } + } else { + this.handlePasteText(editor, pastedTxt) + } + } + + if (!this.props.mode && this.props.autoDetect) { + this.autoDetectLanguage(editor.doc.getValue()) } } @@ -568,8 +993,7 @@ export default class CodeEditor extends React.Component { } } - handlePasteUrl (e, editor, pastedTxt) { - e.preventDefault() + handlePasteUrl (editor, pastedTxt) { const taggedUrl = `<${pastedTxt}>` editor.replaceSelection(taggedUrl) @@ -608,6 +1032,15 @@ export default class CodeEditor extends React.Component { }) } + handlePasteHtml (editor, pastedHtml) { + const markdown = this.turndownService.turndown(pastedHtml) + editor.replaceSelection(markdown) + } + + handlePasteText (editor, pastedTxt) { + editor.replaceSelection(pastedTxt) + } + mapNormalResponse (response, pastedTxt) { return this.decodeResponse(response).then(body => { return new Promise((resolve, reject) => { @@ -628,6 +1061,29 @@ export default class CodeEditor extends React.Component { }) } + initialHighlighting () { + if (this.editor.options.linesHighlighted == null) { + return + } + + const totalHighlightedLines = this.editor.options.linesHighlighted.length + const totalAvailableLines = this.editor.lineCount() + + for (let i = 0; i < totalHighlightedLines; i++) { + const lineNumber = this.editor.options.linesHighlighted[i] + if (lineNumber > totalAvailableLines) { + // make sure that we skip the invalid lines althrough this case should not be happened. + continue + } + this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background') + } + } + + restartHighlighting () { + this.editor.options.linesHighlighted = this.props.linesHighlighted + this.initialHighlighting() + } + mapImageResponse (response, pastedTxt) { return new Promise((resolve, reject) => { try { @@ -673,23 +1129,50 @@ export default class CodeEditor extends React.Component { } render () { - const {className, fontSize} = this.props + const { + className, + fontSize + } = this.props const fontFamily = normalizeEditorFontFamily(this.props.fontFamily) const width = this.props.width - return ( -
this.handleDropImage(e)} + return (< + div className={ + className == null ? 'CodeEditor' : `CodeEditor ${className}` + } + ref='root' + tabIndex='-1' + style={ + { + fontFamily, + fontSize: fontSize, + width: width + } + } + onDrop={ + e => this.handleDropImage(e) + } /> ) } + + createSpellCheckPanel () { + 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 +1183,9 @@ CodeEditor.propTypes = { className: PropTypes.string, onBlur: PropTypes.func, onChange: PropTypes.func, - readOnly: PropTypes.bool + readOnly: PropTypes.bool, + autoDetect: PropTypes.bool, + spellCheck: PropTypes.bool } CodeEditor.defaultProps = { @@ -710,5 +1195,7 @@ CodeEditor.defaultProps = { fontSize: 14, fontFamily: 'Monaco, Consolas', indentSize: 4, - indentType: 'space' + indentType: 'space', + autoDetect: false, + 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/ColorPicker.js b/browser/components/ColorPicker.js new file mode 100644 index 00000000..9e0199c2 --- /dev/null +++ b/browser/components/ColorPicker.js @@ -0,0 +1,68 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { SketchPicker } from 'react-color' +import CSSModules from 'browser/lib/CSSModules' +import styles from './ColorPicker.styl' + +const componentHeight = 330 + +class ColorPicker extends React.Component { + constructor (props) { + super(props) + + this.state = { + color: this.props.color || '#939395' + } + + this.onColorChange = this.onColorChange.bind(this) + this.handleConfirm = this.handleConfirm.bind(this) + } + + componentWillReceiveProps (nextProps) { + this.onColorChange(nextProps.color) + } + + onColorChange (color) { + this.setState({ + color + }) + } + + handleConfirm () { + this.props.onConfirm(this.state.color) + } + + render () { + const { onReset, onCancel, targetRect } = this.props + const { color } = this.state + + const clientHeight = document.body.clientHeight + const alignX = targetRect.right + 4 + let alignY = targetRect.top + if (targetRect.top + componentHeight > clientHeight) { + alignY = targetRect.bottom - componentHeight + } + + return ( +
+
+ +
+ + + +
+
+ ) + } +} + +ColorPicker.propTypes = { + color: PropTypes.string, + targetRect: PropTypes.object, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + onReset: PropTypes.func.isRequired +} + +export default CSSModules(ColorPicker, styles) diff --git a/browser/components/ColorPicker.styl b/browser/components/ColorPicker.styl new file mode 100644 index 00000000..fbc1212a --- /dev/null +++ b/browser/components/ColorPicker.styl @@ -0,0 +1,39 @@ +.colorPicker + position fixed + z-index 2 + display flex + flex-direction column + +.cover + position fixed + top 0 + right 0 + bottom 0 + left 0 + +.footer + display flex + justify-content center + z-index 2 + align-items center + & > button + button + margin-left 10px + +.btn-cancel, +.btn-confirm, +.btn-reset + vertical-align middle + height 25px + margin-top 2.5px + border-radius 2px + border none + padding 0 5px + background-color $default-button-background + &:hover + background-color $default-button-background--hover +.btn-confirm + background-color #1EC38B + &:hover + background-color darken(#1EC38B, 25%) + + diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 70df16a0..466be3fa 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -6,6 +6,8 @@ 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' +import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' class MarkdownEditor extends React.Component { constructor (props) { @@ -18,10 +20,10 @@ class MarkdownEditor extends React.Component { this.supportMdSelectionBold = [16, 17, 186] this.state = { - status: 'PREVIEW', + status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE', renderValue: props.value, keyPressed: new Set(), - isLocked: false + isLocked: props.isLocked } this.lockEditorCode = () => this.handleLockEditor() @@ -64,17 +66,20 @@ class MarkdownEditor extends React.Component { }) } + setValue (value) { + this.refs.code.setValue(value) + } + handleChange (e) { this.value = this.refs.code.value this.props.onChange(e) } handleContextMenu (e) { + if (this.state.isLocked) return const { config } = this.props if (config.editor.switchPreview === 'RIGHTCLICK') { - const newStatus = this.state.status === 'PREVIEW' - ? 'CODE' - : 'PREVIEW' + const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW' this.setState({ status: newStatus }, () => { @@ -84,6 +89,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 +149,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 +161,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')) } @@ -212,6 +223,28 @@ class MarkdownEditor extends React.Component { this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`) } + handleDropImage (dropEvent) { + dropEvent.preventDefault() + const { storageKey, noteKey } = this.props + + this.setState({ + status: 'CODE' + }, () => { + this.refs.code.focus() + + this.refs.code.editor.execCommand('goDocEnd') + this.refs.code.editor.execCommand('goLineEnd') + this.refs.code.editor.execCommand('newlineAndIndent') + + attachmentManagement.handleAttachmentDrop( + this.refs.code, + storageKey, + noteKey, + dropEvent + ) + }) + } + handleKeyUp (e) { const keyPressed = this.state.keyPressed keyPressed.delete(e.keyCode) @@ -223,7 +256,7 @@ class MarkdownEditor extends React.Component { } render () { - const {className, value, config, storageKey, noteKey} = this.props + const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props let editorFontSize = parseInt(config.editor.fontSize, 10) if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 @@ -261,13 +294,21 @@ class MarkdownEditor extends React.Component { enableRulers={config.editor.enableRulers} rulers={config.editor.rulers} displayLineNumbers={config.editor.displayLineNumbers} + matchingPairs={config.editor.matchingPairs} + matchingTriples={config.editor.matchingTriples} + explodingPairs={config.editor.explodingPairs} scrollPastEnd={config.editor.scrollPastEnd} storageKey={storageKey} noteKey={noteKey} fetchUrlTitle={config.editor.fetchUrlTitle} enableTableEditor={config.editor.enableTableEditor} + linesHighlighted={linesHighlighted} onChange={(e) => this.handleChange(e)} onBlur={(e) => this.handleBlur(e)} + spellCheck={config.editor.spellcheck} + enableSmartPaste={config.editor.enableSmartPaste} + hotkey={config.hotkey} + switchPreview={config.editor.switchPreview} /> this.handleDropImage(e)} />
) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index e69f312e..4c638dc9 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -17,9 +17,13 @@ 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 { render } from 'react-dom' +import Carousel from 'react-image-carousel' +import ConfigManager from '../main/lib/ConfigManager' const { remote, shell } = require('electron') const attachmentManagement = require('../main/lib/dataApi/attachmentManagement') @@ -38,7 +42,8 @@ const appPath = fileUrl( ) const CSS_FILES = [ `${appPath}/node_modules/katex/dist/katex.min.css`, - `${appPath}/node_modules/codemirror/lib/codemirror.css` + `${appPath}/node_modules/codemirror/lib/codemirror.css`, + `${appPath}/node_modules/react-image-carousel/lib/css/main.min.css` ] function buildStyle ( @@ -80,7 +85,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 +92,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 +153,8 @@ body p { display: none } } + +${allowCustomCSS ? customCSS : ''} ` } @@ -199,7 +210,7 @@ export default class MarkdownPreview extends React.Component { this.saveAsHtmlHandler = () => this.handleSaveAsHtml() this.printHandler = () => this.handlePrint() - this.linkClickHandler = this.handlelinkClick.bind(this) + this.linkClickHandler = this.handleLinkClick.bind(this) this.initMarkdown = this.initMarkdown.bind(this) this.initMarkdown() } @@ -256,6 +267,10 @@ export default class MarkdownPreview extends React.Component { } handleMouseDown (e) { + const config = ConfigManager.get() + if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') { + eventEmitter.emit('topbar:togglemodebutton', 'CODE') + } if (e.target != null) { switch (e.target.tagName) { case 'A': @@ -279,26 +294,7 @@ export default class MarkdownPreview extends React.Component { } handleSaveAsMd () { - this.exportAsDocument('md', (noteContent, exportTasks) => { - let result = noteContent - if (this.props && this.props.storagePath && this.props.noteKey) { - const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( - noteContent, - this.props.storagePath - ) - attachmentsAbsolutePaths.forEach(attachment => { - exportTasks.push({ - src: attachment, - dst: attachmentManagement.DESTINATION_FOLDER - }) - }) - result = attachmentManagement.removeStorageAndNoteReferences( - noteContent, - this.props.noteKey - ) - } - return result - }) + this.exportAsDocument('md') } handleSaveAsHtml () { @@ -325,15 +321,8 @@ export default class MarkdownPreview extends React.Component { allowCustomCSS, customCSS ) - let body = this.markdown.render( - escapeHtmlCharacters(noteContent, { detectCodeBlock: true }) - ) + let body = this.markdown.render(noteContent) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] - const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( - noteContent, - this.props.storagePath - ) - files.forEach(file => { if (global.process.platform === 'win32') { file = file.replace('file:///', '') @@ -345,16 +334,6 @@ export default class MarkdownPreview extends React.Component { dst: 'css' }) }) - attachmentsAbsolutePaths.forEach(attachment => { - exportTasks.push({ - src: attachment, - dst: attachmentManagement.DESTINATION_FOLDER - }) - }) - body = attachmentManagement.removeStorageAndNoteReferences( - body, - this.props.noteKey - ) let styles = '' files.forEach(file => { @@ -387,8 +366,9 @@ export default class MarkdownPreview extends React.Component { if (filename) { const content = this.props.value const storage = this.props.storagePath + const nodeKey = this.props.noteKey - exportNote(storage, content, filename, contentFormatter) + exportNote(nodeKey, storage, content, filename, contentFormatter) .then(res => { dialog.showMessageBox(remote.getCurrentWindow(), { type: 'info', @@ -418,6 +398,31 @@ export default class MarkdownPreview extends React.Component { } } + /** + * @description Convert special characters between three ``` + * @param {string[]} splitWithCodeTag Array of HTML strings separated by three ``` + * @returns {string} HTML in which special characters between three ``` have been converted + */ + escapeHtmlCharactersInCodeTag (splitWithCodeTag) { + for (let index = 0; index < splitWithCodeTag.length; index++) { + const codeTagRequired = (splitWithCodeTag[index] !== '\`\`\`' && index < splitWithCodeTag.length - 1) + if (codeTagRequired) { + splitWithCodeTag.splice((index + 1), 0, '\`\`\`') + } + } + let inCodeTag = false + let result = '' + for (let content of splitWithCodeTag) { + if (content === '\`\`\`') { + inCodeTag = !inCodeTag + } else if (inCodeTag) { + content = escapeHtmlCharacters(content) + } + result += content + } + return result + } + getScrollBarStyle () { const { theme } = this.props @@ -433,6 +438,8 @@ export default class MarkdownPreview extends React.Component { } componentDidMount () { + const { onDrop } = this.props + this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.contentWindow.document.body.addEventListener( 'contextmenu', @@ -470,7 +477,7 @@ export default class MarkdownPreview extends React.Component { ) this.refs.root.contentWindow.document.addEventListener( 'drop', - this.preventImageDroppedHandler + onDrop || this.preventImageDroppedHandler ) this.refs.root.contentWindow.document.addEventListener( 'dragover', @@ -484,13 +491,11 @@ export default class MarkdownPreview extends React.Component { eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler) eventEmitter.on('print', this.printHandler) - eventEmitter.on('config-renew', () => { - this.markdown.updateConfig() - this.rewriteIframe() - }) } componentWillUnmount () { + const { onDrop } = this.props + this.refs.root.contentWindow.document.body.removeEventListener( 'contextmenu', this.contextMenuHandler @@ -509,7 +514,7 @@ export default class MarkdownPreview extends React.Component { ) this.refs.root.contentWindow.document.removeEventListener( 'drop', - this.preventImageDroppedHandler + onDrop || this.preventImageDroppedHandler ) this.refs.root.contentWindow.document.removeEventListener( 'dragover', @@ -531,7 +536,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() @@ -651,11 +657,16 @@ export default class MarkdownPreview extends React.Component { indentSize, showCopyNotification, storagePath, - noteKey + noteKey, + sanitize } = this.props let { value, codeBlockTheme } = this.props this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) + if (sanitize === 'NONE') { + const splitWithCodeTag = value.split('```') + value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag) + } const renderedHTML = this.markdown.render(value) attachmentManagement.migrateAttachments(value, storagePath, noteKey) this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS( @@ -737,7 +748,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 } @@ -758,7 +768,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 } @@ -769,14 +778,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 } @@ -788,6 +804,34 @@ export default class MarkdownPreview extends React.Component { mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme) } ) + + _.forEach( + this.refs.root.contentWindow.document.querySelectorAll('.gallery'), + el => { + const images = el.innerHTML.split(/\n/g).filter(i => i.length > 0) + el.innerHTML = '' + + const height = el.attributes.getNamedItem('data-height') + if (height && height.value !== 'undefined') { + el.style.height = height.value + 'vh' + } + + let autoplay = el.attributes.getNamedItem('data-autoplay') + if (autoplay && autoplay.value !== 'undefined') { + autoplay = parseInt(autoplay.value, 10) || 0 + } else { + autoplay = 0 + } + + render( + , + el + ) + } + ) } focus () { @@ -830,7 +874,7 @@ export default class MarkdownPreview extends React.Component { return new window.Notification(title, options) } - handlelinkClick (e) { + handleLinkClick (e) { e.preventDefault() e.stopPropagation() @@ -860,6 +904,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 d714125a..4477288a 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -20,12 +20,18 @@ class MarkdownSplitEditor extends React.Component { } } - handleOnChange () { + setValue (value) { + this.refs.code.setValue(value) + } + + handleOnChange (e) { this.value = this.refs.code.value - this.props.onChange() + this.props.onChange(e) } 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')) } @@ -128,7 +136,7 @@ class MarkdownSplitEditor extends React.Component { } render () { - const {config, value, storageKey, noteKey} = this.props + const {config, value, storageKey, noteKey, linesHighlighted} = this.props const storage = findStorage(storageKey) let editorFontSize = parseInt(config.editor.fontSize, 10) if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 @@ -152,6 +160,9 @@ class MarkdownSplitEditor extends React.Component { fontFamily={config.editor.fontFamily} fontSize={editorFontSize} displayLineNumbers={config.editor.displayLineNumbers} + matchingPairs={config.editor.matchingPairs} + matchingTriples={config.editor.matchingTriples} + explodingPairs={config.editor.explodingPairs} indentType={config.editor.indentType} indentSize={editorIndentSize} enableRulers={config.editor.enableRulers} @@ -161,8 +172,13 @@ class MarkdownSplitEditor extends React.Component { enableTableEditor={config.editor.enableTableEditor} storageKey={storageKey} noteKey={noteKey} - onChange={this.handleOnChange.bind(this)} + linesHighlighted={linesHighlighted} + onChange={(e) => this.handleOnChange(e)} onScroll={this.handleScroll.bind(this)} + spellCheck={config.editor.spellcheck} + enableSmartPaste={config.editor.enableSmartPaste} + hotkey={config.hotkey} + switchPreview={config.editor.switchPreview} />
this.handleMouseDown(e)} >
@@ -192,6 +208,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..625bb38d 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types' import React from 'react' import { isArray } from 'lodash' +import invertColor from 'invert-color' import CSSModules from 'browser/lib/CSSModules' import { getTodoStatus } from 'browser/lib/getTodoStatus' import styles from './NoteItem.styl' @@ -13,27 +14,39 @@ import i18n from 'browser/lib/i18n' /** * @description Tag element component. * @param {string} tagName + * @param {string} color * @return {React.Component} */ -const TagElement = ({ tagName }) => ( - - #{tagName} - -) +const TagElement = ({ tagName, color }) => { + const style = {} + if (color) { + style.backgroundColor = color + style.color = invertColor(color, { black: '#222', white: '#f1f1f1', threshold: 0.3 }) + } + return ( + + #{tagName} + + ) +} /** * @description Tag element list component. * @param {Array|null} tags + * @param {boolean} showTagsAlphabetically + * @param {Object} coloredTags * @return {React.Component} */ -const TagElementList = tags => { +const TagElementList = (tags, showTagsAlphabetically, coloredTags) => { if (!isArray(tags)) { return [] } - const tagElements = tags.map(tag => TagElement({ tagName: tag })) - - return tagElements + if (showTagsAlphabetically) { + return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) + } else { + return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) + } } /** @@ -43,6 +56,7 @@ const TagElementList = tags => { * @param {Function} handleNoteClick * @param {Function} handleNoteContextMenu * @param {Function} handleDragStart + * @param {Object} coloredTags * @param {string} dateDisplay */ const NoteItem = ({ @@ -55,7 +69,9 @@ const NoteItem = ({ pathname, storageName, folderName, - viewType + viewType, + showTagsAlphabetically, + coloredTags }) => (
{note.tags.length > 0 - ? TagElementList(note.tags) + ? TagElementList(note.tags, showTagsAlphabetically, coloredTags) :
{this.SideNavComponent(isFolded, storageList)} + {colorPicker}
) } diff --git a/browser/main/StatusBar/StatusBar.styl b/browser/main/StatusBar/StatusBar.styl index 83cf2088..23dec208 100644 --- a/browser/main/StatusBar/StatusBar.styl +++ b/browser/main/StatusBar/StatusBar.styl @@ -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 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/index.js b/browser/main/TopBar/index.js index a5687ecb..91256daf 100644 --- a/browser/main/TopBar/index.js +++ b/browser/main/TopBar/index.js @@ -6,6 +6,7 @@ import _ from 'lodash' import ee from 'browser/main/lib/eventEmitter' import NewNoteButton from 'browser/main/NewNoteButton' import i18n from 'browser/lib/i18n' +import debounce from 'lodash/debounce' class TopBar extends React.Component { constructor (props) { @@ -25,6 +26,10 @@ class TopBar extends React.Component { } this.codeInitHandler = this.handleCodeInit.bind(this) + + this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, { + maxWait: 1000 / 8 + }) } componentDidMount () { @@ -94,7 +99,6 @@ class TopBar extends React.Component { } handleKeyUp (e) { - const { router } = this.context // reset states this.setState({ isConfirmTranslation: false @@ -106,21 +110,21 @@ class TopBar extends React.Component { isConfirmTranslation: true }) const keyword = this.refs.searchInput.value - router.push(`/searched/${encodeURIComponent(keyword)}`) - this.setState({ - search: keyword - }) + this.updateKeyword(keyword) } } handleSearchChange (e) { - const { router } = this.context - const keyword = this.refs.searchInput.value if (this.state.isAlphabet || this.state.isConfirmTranslation) { - router.push(`/searched/${encodeURIComponent(keyword)}`) + const keyword = this.refs.searchInput.value + this.updateKeyword(keyword) } else { e.preventDefault() } + } + + updateKeyword (keyword) { + this.context.router.push(`/searched/${encodeURIComponent(keyword)}`) this.setState({ search: keyword }) diff --git a/browser/main/global.styl b/browser/main/global.styl index e04060c2..d864993d 100644 --- a/browser/main/global.styl +++ b/browser/main/global.styl @@ -97,6 +97,7 @@ modalBackColor = white body[data-theme="dark"] + background-color $ui-dark-backgroundColor ::-webkit-scrollbar-thumb background-color rgba(0, 0, 0, 0.3) .ModalBase @@ -148,6 +149,7 @@ body[data-theme="dark"] z-index modalZIndex + 5 body[data-theme="solarized-dark"] + background-color $ui-solarized-dark-backgroundColor ::-webkit-scrollbar-thumb background-color rgba(0, 0, 0, 0.3) .ModalBase @@ -157,6 +159,7 @@ body[data-theme="solarized-dark"] color: $ui-solarized-dark-text-color body[data-theme="monokai"] + background-color $ui-monokai-backgroundColor ::-webkit-scrollbar-thumb background-color rgba(0, 0, 0, 0.3) .ModalBase @@ -166,6 +169,7 @@ body[data-theme="monokai"] color: $ui-monokai-text-color body[data-theme="dracula"] + background-color $ui-dracula-backgroundColor ::-webkit-scrollbar-thumb background-color rgba(0, 0, 0, 0.3) .ModalBase 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 1727deb8..8a89c9a9 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -24,14 +24,18 @@ export const DEFAULT_CONFIG = { amaEnabled: true, hotkey: { toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E', - toggleMode: OSX ? 'Command + Option + M' : 'Ctrl + M' + toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', + deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace', + pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V', + toggleMenuBar: 'Alt' }, ui: { language: 'en', theme: 'default', showCopyNotification: true, disableDirectWrite: false, - defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE' + defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE' + showMenuBar: false }, editor: { theme: 'base16-light', @@ -43,11 +47,19 @@ export const DEFAULT_CONFIG = { enableRulers: false, rulers: [80, 120], displayLineNumbers: true, - switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR + matchingPairs: '()[]{}\'\'""$$**``', + matchingTriples: '```"""\'\'\'', + explodingPairs: '[]{}``$$', + switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK' + delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE' scrollPastEnd: false, - type: 'SPLIT', + type: 'SPLIT', // 'SPLIT', 'EDITOR_PREVIEW' fetchUrlTitle: true, - enableTableEditor: false + enableTableEditor: false, + enableFrontMatterTitle: true, + frontMatterTitleField: 'title', + spellcheck: false, + enableSmartPaste: false }, preview: { fontSize: '14', @@ -60,6 +72,7 @@ export const DEFAULT_CONFIG = { latexBlockClose: '$$', plantUMLServerAddress: 'http://www.plantuml.com/plantuml', scrollPastEnd: false, + scrollSync: true, smartQuotes: true, breaks: true, smartArrows: false, @@ -75,7 +88,8 @@ export const DEFAULT_CONFIG = { token: '', username: '', password: '' - } + }, + coloredTags: {} } function validate (config) { @@ -197,8 +211,8 @@ function assignConfigValues (originalConfig, rcConfig) { 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(/Cmd\s/g, 'Command ') + 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..6a0315f7 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -227,7 +227,15 @@ function migrateAttachments (markdownContent, storagePath, noteKey) { * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths. */ function fixLocalURLS (renderedHTML, storagePath) { - return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) { + /* + A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`. + + - `STORAGE_FOLDER_PLACEHOLDER` will match `:storage` + - `(?:(?:\\\/|%5C)[\\w.]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg` + - `(?:\\\/|%5C)[\\w.]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg` + - `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows. + */ + return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[\\w.]+)+', 'g'), function (match) { var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g') return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER)) }) @@ -316,6 +324,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem reader.readAsDataURL(blob) } +/** + * @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code + * @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code + * @param {String} storageKey Key of the current storage + * @param {String} noteKey Key of the current note + * @param {NativeImage} image The native image + */ +function handlePastNativeImage (codeEditor, storageKey, noteKey, image) { + if (!codeEditor) { + throw new Error('codeEditor has to be given') + } + if (!storageKey) { + throw new Error('storageKey has to be given') + } + + if (!noteKey) { + throw new Error('noteKey has to be given') + } + if (!image) { + throw new Error('image has to be given') + } + + const targetStorage = findStorage.findStorage(storageKey) + const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + + createAttachmentDestinationFolder(targetStorage.path, noteKey) + + const imageName = `${uniqueSlug()}.png` + const imagePath = path.join(destinationDir, imageName) + + const binaryData = image.toPNG() + fs.writeFileSync(imagePath, binaryData, 'binary') + + const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName) + const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true) + codeEditor.insertAttachmentMd(imageMd) +} + /** * @description Returns all attachment paths of the given markdown * @param {String} markdownContent content in which the attachment paths should be found @@ -529,7 +575,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) { return modifiedLinkText }) } else { - console.log('One if the parameters was null -> Do nothing..') return Promise.resolve(linkText) } } @@ -540,6 +585,7 @@ module.exports = { generateAttachmentMarkdown, handleAttachmentDrop, handlePastImageEvent, + handlePastNativeImage, getAttachmentsInMarkdownContent, getAbsolutePathsOfAttachmentsInContent, removeStorageAndNoteReferences, diff --git a/browser/main/lib/dataApi/copyFile.js b/browser/main/lib/dataApi/copyFile.js index 2dc66309..6f23aae2 100755 --- a/browser/main/lib/dataApi/copyFile.js +++ b/browser/main/lib/dataApi/copyFile.js @@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) { const dstFolder = path.dirname(dstPath) if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder) - const input = fs.createReadStream(srcPath) + const input = fs.createReadStream(decodeURI(srcPath)) const output = fs.createWriteStream(dstPath) output.on('error', reject) diff --git a/browser/main/lib/dataApi/createNote.js b/browser/main/lib/dataApi/createNote.js index e5d44489..5bfa2457 100644 --- a/browser/main/lib/dataApi/createNote.js +++ b/browser/main/lib/dataApi/createNote.js @@ -16,6 +16,7 @@ function validateInput (input) { switch (input.type) { case 'MARKDOWN_NOTE': if (!_.isString(input.content)) input.content = '' + if (!_.isArray(input.linesHighlighted)) input.linesHighlighted = [] break case 'SNIPPET_NOTE': if (!_.isString(input.description)) input.description = '' @@ -23,7 +24,8 @@ function validateInput (input) { input.snippets = [{ name: '', mode: 'text', - content: '' + content: '', + linesHighlighted: [] }] } break diff --git a/browser/main/lib/dataApi/createSnippet.js b/browser/main/lib/dataApi/createSnippet.js index 5d189217..2e585c9f 100644 --- a/browser/main/lib/dataApi/createSnippet.js +++ b/browser/main/lib/dataApi/createSnippet.js @@ -9,7 +9,8 @@ function createSnippet (snippetFile) { id: crypto.randomBytes(16).toString('hex'), name: 'Unnamed snippet', prefix: [], - content: '' + content: '', + linesHighlighted: [] } fetchSnippet(null, snippetFile).then((snippets) => { snippets.push(newSnippet) diff --git a/browser/main/lib/dataApi/exportFolder.js b/browser/main/lib/dataApi/exportFolder.js index 3e998f15..771f77dc 100644 --- a/browser/main/lib/dataApi/exportFolder.js +++ b/browser/main/lib/dataApi/exportFolder.js @@ -1,9 +1,9 @@ import { findStorage } from 'browser/lib/findStorage' import resolveStorageData from './resolveStorageData' import resolveStorageNotes from './resolveStorageNotes' +import exportNote from './exportNote' import filenamify from 'filenamify' import * as path from 'path' -import * as fs from 'fs' /** * @param {String} storageKey @@ -45,9 +45,9 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) { notes .filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE') - .forEach(snippet => { - const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`) - fs.writeFileSync(notePath, snippet.content) + .forEach(note => { + const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`) + exportNote(note.key, storage.path, note.content, notePath, null) }) return { diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index e4fec5f4..b358e548 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -4,27 +4,43 @@ import { findStorage } from 'browser/lib/findStorage' const fs = require('fs') const path = require('path') +const attachmentManagement = require('./attachmentManagement') + /** - * Export note together with images + * Export note together with attachments * - * If images is stored in the storage, creates 'images' subfolder in target directory - * and copies images to it. Changes links to images in the content of the note + * If attachments are stored in the storage, creates 'attachments' subfolder in target directory + * and copies attachments to it. Changes links to images in the content of the note * + * @param {String} nodeKey key of the node that should be exported * @param {String} storageKey or storage path * @param {String} noteContent Content to export * @param {String} targetPath Path to exported file * @param {function} outputFormatter * @return {Promise.<*[]>} */ -function exportNote (storageKey, noteContent, targetPath, outputFormatter) { +function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) { const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path const exportTasks = [] if (!storagePath) { throw new Error('Storage path is not found') } + const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( + noteContent, + storagePath + ) + attachmentsAbsolutePaths.forEach(attachment => { + exportTasks.push({ + src: attachment, + dst: attachmentManagement.DESTINATION_FOLDER + }) + }) - let exportedData = noteContent + let exportedData = attachmentManagement.removeStorageAndNoteReferences( + noteContent, + nodeKey + ) if (outputFormatter) { exportedData = outputFormatter(exportedData, exportTasks) diff --git a/browser/main/lib/dataApi/init.js b/browser/main/lib/dataApi/init.js index 7f81e90b..0dbcc182 100644 --- a/browser/main/lib/dataApi/init.js +++ b/browser/main/lib/dataApi/init.js @@ -4,6 +4,7 @@ const resolveStorageData = require('./resolveStorageData') const resolveStorageNotes = require('./resolveStorageNotes') const consts = require('browser/lib/consts') const path = require('path') +const fs = require('fs') const CSON = require('@rokt33r/season') /** * @return {Object} all storages and notes @@ -19,11 +20,14 @@ const CSON = require('@rokt33r/season') * 2. legacy * 3. empty directory */ + function init () { const fetchStorages = function () { let rawStorages try { rawStorages = JSON.parse(window.localStorage.getItem('storages')) + // Remove storages who's location is inaccesible. + rawStorages = rawStorages.filter(storage => fs.existsSync(storage.path)) if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.') } catch (e) { console.warn('Failed to parse cached data from localStorage', e) @@ -36,6 +40,7 @@ function init () { const fetchNotes = function (storages) { const findNotesFromEachStorage = storages + .filter(storage => fs.existsSync(storage.path)) .map((storage) => { return resolveStorageNotes(storage) .then((notes) => { @@ -51,7 +56,11 @@ function init () { } }) if (unknownCount > 0) { - CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version'])) + try { + CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version'])) + } catch (e) { + console.log('Error writting boostnote.json: ' + e + ' from init.js') + } } return notes }) diff --git a/browser/main/lib/dataApi/migrateFromV5Storage.js b/browser/main/lib/dataApi/migrateFromV5Storage.js index b11e66e9..78d78746 100644 --- a/browser/main/lib/dataApi/migrateFromV5Storage.js +++ b/browser/main/lib/dataApi/migrateFromV5Storage.js @@ -69,7 +69,8 @@ function importAll (storage, data) { isStarred: false, title: article.title, content: '# ' + article.title + '\n\n' + article.content, - key: noteKey + key: noteKey, + linesHighlighted: article.linesHighlighted } notes.push(newNote) } else { @@ -87,7 +88,8 @@ function importAll (storage, data) { snippets: [{ name: article.mode, mode: article.mode, - content: article.content + content: article.content, + linesHighlighted: article.linesHighlighted }] } notes.push(newNote) 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/dataApi/updateNote.js b/browser/main/lib/dataApi/updateNote.js index 147fbc06..ce9fabcf 100644 --- a/browser/main/lib/dataApi/updateNote.js +++ b/browser/main/lib/dataApi/updateNote.js @@ -39,6 +39,9 @@ function validateInput (input) { if (input.content != null) { if (!_.isString(input.content)) validatedInput.content = '' else validatedInput.content = input.content + + if (!_.isArray(input.linesHighlighted)) validatedInput.linesHighlighted = [] + else validatedInput.linesHighlighted = input.linesHighlighted } return validatedInput case 'SNIPPET_NOTE': @@ -51,7 +54,8 @@ function validateInput (input) { validatedInput.snippets = [{ name: '', mode: 'text', - content: '' + content: '', + linesHighlighted: [] }] } else { validatedInput.snippets = input.snippets @@ -96,12 +100,14 @@ function updateNote (storageKey, noteKey, input) { snippets: [{ name: '', mode: 'text', - content: '' + content: '', + linesHighlighted: [] }] } : { type: 'MARKDOWN_NOTE', - content: '' + content: '', + linesHighlighted: [] } noteData.title = '' if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.') diff --git a/browser/main/lib/dataApi/updateSnippet.js b/browser/main/lib/dataApi/updateSnippet.js index f2310b8e..f132d83f 100644 --- a/browser/main/lib/dataApi/updateSnippet.js +++ b/browser/main/lib/dataApi/updateSnippet.js @@ -12,7 +12,8 @@ function updateSnippet (snippet, snippetFile) { if ( currentSnippet.name === snippet.name && currentSnippet.prefix === snippet.prefix && - currentSnippet.content === snippet.content + currentSnippet.content === snippet.content && + currentSnippet.linesHighlighted === snippet.linesHighlighted ) { // if everything is the same then don't write to disk resolve(snippets) @@ -20,6 +21,7 @@ function updateSnippet (snippet, snippetFile) { currentSnippet.name = snippet.name currentSnippet.prefix = snippet.prefix currentSnippet.content = snippet.content + currentSnippet.linesHighlighted = (snippet.linesHighlighted) fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => { if (err) reject(err) resolve(snippets) 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..3165606a 100644 --- a/browser/main/lib/shortcut.js +++ b/browser/main/lib/shortcut.js @@ -3,5 +3,11 @@ import ee from 'browser/main/lib/eventEmitter' module.exports = { 'toggleMode': () => { ee.emit('topbar:togglemodebutton') + }, + 'deleteNote': () => { + ee.emit('hotkey:deletenote') + }, + 'toggleMenuBar': () => { + ee.emit('menubar:togglemenubar') } } 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/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/FolderItem.styl b/browser/main/modals/PreferencesModal/FolderItem.styl index 2ded3ada..618e9bc4 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.styl +++ b/browser/main/modals/PreferencesModal/FolderItem.styl @@ -62,7 +62,7 @@ .folderItem-right-button vertical-align middle height 25px - margin-top 2.5px + margin-top 2px colorDefaultButton() border-radius 2px border $ui-border diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 1c40a13a..218a68f6 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,10 @@ class HotkeyTab extends React.Component { const { config } = this.state config.hotkey = { toggleMain: this.refs.toggleMain.value, - toggleMode: this.refs.toggleMode.value + toggleMode: this.refs.toggleMode.value, + deleteNote: this.refs.deleteNote.value, + pasteSmartly: this.refs.pasteSmartly.value, + toggleMenuBar: this.refs.toggleMenuBar.value } this.setState({ config @@ -116,6 +129,17 @@ class HotkeyTab extends React.Component { />
+
+
{i18n.__('Show/Hide Menu Bar')}
+
+ this.handleHotkeyChange(e)} + ref='toggleMenuBar' + value={config.hotkey.toggleMenuBar} + type='text' + /> +
+
{i18n.__('Toggle Editor Mode')}
@@ -127,6 +151,28 @@ class HotkeyTab extends React.Component { />
+
+
{i18n.__('Delete Note')}
+
+ this.handleHotkeyChange(e)} + ref='deleteNote' + value={config.hotkey.deleteNote} + type='text' + /> +
+
+
+
{i18n.__('Paste HTML')}
+
+ this.handleHotkeyChange(e)} + ref='pasteSmartly' + value={config.hotkey.pasteSmartly} + type='text' + /> +
+
+
+ +
-
- -
{ global.process.platform === 'win32' ?
@@ -271,6 +284,64 @@ class UiTab extends React.Component {
: null } + +
Tags
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
Editor
@@ -421,6 +492,7 @@ class UiTab extends React.Component { ref='editorSnippetDefaultLanguage' onChange={(e) => this.handleUIChange(e)} > + { _.sortBy(CodeMirror.modeInfo.map(mode => mode.name)).map(name => ()) } @@ -428,6 +500,31 @@ class UiTab extends React.Component {
+
+
+ {i18n.__('Front matter title field')} +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+ {i18n.__('Matching character pairs')} +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+ +
+
+ {i18n.__('Matching character triples')} +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+ +
+
+ {i18n.__('Exploding character pairs')} +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+
{i18n.__('Preview')}
@@ -499,6 +660,7 @@ class UiTab extends React.Component { />
+
{i18n.__('Code Block Theme')}
@@ -534,6 +696,16 @@ class UiTab extends React.Component { {i18n.__('Allow preview to scroll past the last line')}
+
+ +