diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..a742a59e
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,41 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "BoostNote Main",
+ "protocol": "inspector",
+ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
+ "runtimeArgs": [
+ "--remote-debugging-port=9223",
+ "--hot",
+ "${workspaceFolder}/index.js"
+ ],
+ "windows": {
+ "runtimeExecutable": "${workspaceFolder}/node_modeules/.bin/electron.cmd"
+ }
+ },
+ {
+ "type": "chrome",
+ "request": "attach",
+ "name": "BoostNote Renderer",
+ "port": 9223,
+ "webRoot": "${workspaceFolder}",
+ "sourceMapPathOverrides": {
+ "webpack:///./~/*": "${webRoot}/node_modules/*",
+ "webpack:///*": "${webRoot}/*"
+ }
+ }
+ ],
+ "compounds": [
+ {
+ "name": "BostNote All",
+ "configurations": ["BoostNote Main", "BoostNote Renderer"]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..c6664225
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,27 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build Boostnote",
+ "group": "build",
+ "type": "npm",
+ "script": "watch",
+ "isBackground": true,
+ "presentation": {
+ "reveal": "always",
+ },
+ "problemMatcher": {
+ "pattern":[
+ {
+ "regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$",
+ "file": 1,
+ "location": 2,
+ "message": 3
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
index 1f5ada57..5554c4b8 100644
--- a/ISSUE_TEMPLATE.md
+++ b/ISSUE_TEMPLATE.md
@@ -1,15 +1,25 @@
# Current behavior
# Expected behavior
+
+
# Steps to reproduce
+
+
1.
2.
3.
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..58df576a
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,36 @@
+
+## Description
+
+
+## Issue fixed
+
+
+
+## Type of changes
+
+- :white_circle: Bug fix (Change that fixed an issue)
+- :white_circle: Breaking change (Change that can cause existing functionality to change)
+- :white_circle: Improvement (Change that improves the code. Maybe performance or development improvement)
+- :white_circle: Feature (Change that adds new functionality)
+- :white_circle: Documentation change (Change that modifies documentation. Maybe typo fixes)
+
+## Checklist:
+
+- :white_circle: My code follows [the project code style](docs/code_style.md)
+- :white_circle: I have written test for my code and it has been tested
+- :white_circle: All existing tests have been passed
+- :white_circle: I have attached a screenshot/video to visualize my change if possible
diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index c36a50c1..130cc86e 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -11,9 +11,14 @@ import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
import crypto from 'crypto'
import consts from 'browser/lib/consts'
+import styles from '../components/CodeEditor.styl'
import fs from 'fs'
-const { ipcRenderer } = require('electron')
+const { ipcRenderer, remote } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
+const spellcheck = require('browser/lib/spellcheck')
+const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
+import TurndownService from 'turndown'
+import { gfm } from 'turndown-plugin-gfm'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
@@ -28,7 +33,7 @@ export default class CodeEditor extends React.Component {
leading: false,
trailing: true
})
- this.changeHandler = e => this.handleChange(e)
+ this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
this.focusHandler = () => {
ipcRenderer.send('editor:focused', true)
}
@@ -57,9 +62,18 @@ export default class CodeEditor extends React.Component {
}
this.searchHandler = (e, msg) => this.handleSearch(msg)
this.searchState = null
+ this.scrollToLineHandeler = this.scrollToLine.bind(this)
this.formatTable = () => this.handleFormatTable()
+ this.contextMenuHandler = function (editor, event) {
+ const menu = buildEditorContextMenu(editor, event)
+ if (menu != null) {
+ setTimeout(() => menu.popup(remote.getCurrentWindow()), 30)
+ }
+ }
this.editorActivityHandler = () => this.handleEditorActivity()
+
+ this.turndownService = new TurndownService()
}
handleSearch (msg) {
@@ -125,6 +139,7 @@ export default class CodeEditor extends React.Component {
componentDidMount () {
const { rulers, enableRulers } = this.props
const expandSnippet = this.expandSnippet.bind(this)
+ eventEmitter.on('line:jump', this.scrollToLineHandeler)
const defaultSnippet = [
{
@@ -226,6 +241,7 @@ export default class CodeEditor extends React.Component {
this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler)
this.editor.on('paste', this.pasteHandler)
+ this.editor.on('contextmenu', this.contextMenuHandler)
eventEmitter.on('top:search', this.searchHandler)
eventEmitter.emit('code:init')
@@ -242,6 +258,10 @@ export default class CodeEditor extends React.Component {
this.textEditorInterface = new TextEditorInterface(this.editor)
this.tableEditor = new TableEditor(this.textEditorInterface)
+ if (this.props.spellCheck) {
+ this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
+ }
+
eventEmitter.on('code:format-table', this.formatTable)
this.tableEditorOptions = options({
@@ -311,22 +331,28 @@ export default class CodeEditor extends React.Component {
const snippetLines = snippets[i].content.split('\n')
let cursorLineNumber = 0
let cursorLinePosition = 0
+
+ let cursorIndex
for (let j = 0; j < snippetLines.length; j++) {
- const cursorIndex = snippetLines[j].indexOf(templateCursorString)
+ cursorIndex = snippetLines[j].indexOf(templateCursorString)
+
if (cursorIndex !== -1) {
cursorLineNumber = j
cursorLinePosition = cursorIndex
- cm.replaceRange(
- snippets[i].content.replace(templateCursorString, ''),
- wordBeforeCursor.range.from,
- wordBeforeCursor.range.to
- )
- cm.setCursor({
- line: cursor.line + cursorLineNumber,
- ch: cursorLinePosition
- })
+
+ break
}
}
+
+ cm.replaceRange(
+ snippets[i].content.replace(templateCursorString, ''),
+ wordBeforeCursor.range.from,
+ wordBeforeCursor.range.to
+ )
+ cm.setCursor({
+ line: cursor.line + cursorLineNumber,
+ ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
+ })
} else {
cm.replaceRange(
snippets[i].content,
@@ -383,9 +409,11 @@ export default class CodeEditor extends React.Component {
this.editor.off('paste', this.pasteHandler)
eventEmitter.off('top:search', this.searchHandler)
this.editor.off('scroll', this.scrollHandler)
+ this.editor.off('contextmenu', this.contextMenuHandler)
const editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler)
+ spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED)
eventEmitter.off('code:format-table', this.formatTable)
}
@@ -453,6 +481,16 @@ export default class CodeEditor extends React.Component {
needRefresh = true
}
+ if (prevProps.spellCheck !== this.props.spellCheck) {
+ if (this.props.spellCheck === false) {
+ spellcheck.setLanguage(this.editor, spellcheck.SPELLCHECK_DISABLED)
+ let elem = document.getElementById('editor-bottom-panel')
+ elem.parentNode.removeChild(elem)
+ } else {
+ this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
+ }
+ }
+
if (needRefresh) {
this.editor.refresh()
}
@@ -466,16 +504,23 @@ export default class CodeEditor extends React.Component {
CodeMirror.autoLoadMode(this.editor, syntax.mode)
}
- handleChange (e) {
- this.value = this.editor.getValue()
+ handleChange (editor, changeObject) {
+ spellcheck.handleChange(editor, changeObject)
+ this.value = editor.getValue()
if (this.props.onChange) {
- this.props.onChange(e)
+ this.props.onChange(editor)
}
}
moveCursorTo (row, col) {}
- scrollToLine (num) {}
+ scrollToLine (event, num) {
+ const cursor = {
+ line: num,
+ ch: 1
+ }
+ this.editor.setCursor(cursor)
+ }
focus () {
this.editor.focus()
@@ -538,7 +583,11 @@ export default class CodeEditor extends React.Component {
)
return prevChar === '](' && nextChar === ')'
}
- if (dataTransferItem.type.match('image')) {
+
+ const pastedHtml = clipboardData.getData('text/html')
+ if (pastedHtml !== '') {
+ this.handlePasteHtml(e, editor, pastedHtml)
+ } else if (dataTransferItem.type.match('image')) {
attachmentManagement.handlePastImageEvent(
this,
storageKey,
@@ -608,6 +657,12 @@ export default class CodeEditor extends React.Component {
})
}
+ handlePasteHtml (e, editor, pastedHtml) {
+ e.preventDefault()
+ const markdown = this.turndownService.turndown(pastedHtml)
+ editor.replaceSelection(markdown)
+ }
+
mapNormalResponse (response, pastedTxt) {
return this.decodeResponse(response).then(body => {
return new Promise((resolve, reject) => {
@@ -690,6 +745,25 @@ export default class CodeEditor extends React.Component {
/>
)
}
+
+ createSpellCheckPanel () {
+ const panel = document.createElement('div')
+ panel.className = 'panel bottom'
+ panel.id = 'editor-bottom-panel'
+ const dropdown = document.createElement('select')
+ dropdown.title = 'Spellcheck'
+ dropdown.className = styles['spellcheck-select']
+ dropdown.addEventListener('change', (e) => spellcheck.setLanguage(this.editor, dropdown.value))
+ const options = spellcheck.getAvailableDictionaries()
+ for (const op of options) {
+ const option = document.createElement('option')
+ option.value = op.value
+ option.innerHTML = op.label
+ dropdown.appendChild(option)
+ }
+ panel.appendChild(dropdown)
+ return panel
+ }
}
CodeEditor.propTypes = {
@@ -700,7 +774,8 @@ CodeEditor.propTypes = {
className: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
- readOnly: PropTypes.bool
+ readOnly: PropTypes.bool,
+ spellCheck: PropTypes.bool
}
CodeEditor.defaultProps = {
@@ -710,5 +785,6 @@ CodeEditor.defaultProps = {
fontSize: 14,
fontFamily: 'Monaco, Consolas',
indentSize: 4,
- indentType: 'space'
+ indentType: 'space',
+ spellCheck: false
}
diff --git a/browser/components/CodeEditor.styl b/browser/components/CodeEditor.styl
new file mode 100644
index 00000000..7a254935
--- /dev/null
+++ b/browser/components/CodeEditor.styl
@@ -0,0 +1,6 @@
+.codeEditor-typo
+ text-decoration underline wavy red
+
+.spellcheck-select
+ border: none
+ text-decoration underline wavy red
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index 4c195797..20ce9451 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -6,6 +6,7 @@ import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { findStorage } from 'browser/lib/findStorage'
+import ConfigManager from 'browser/main/lib/ConfigManager'
class MarkdownEditor extends React.Component {
constructor (props) {
@@ -18,7 +19,7 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186]
this.state = {
- status: 'PREVIEW',
+ status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
renderValue: props.value,
keyPressed: new Set(),
isLocked: false
@@ -64,6 +65,10 @@ class MarkdownEditor extends React.Component {
})
}
+ setValue (value) {
+ this.refs.code.setValue(value)
+ }
+
handleChange (e) {
this.value = this.refs.code.value
this.props.onChange(e)
@@ -72,9 +77,7 @@ class MarkdownEditor extends React.Component {
handleContextMenu (e) {
const { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') {
- const newStatus = this.state.status === 'PREVIEW'
- ? 'CODE'
- : 'PREVIEW'
+ const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
this.setState({
status: newStatus
}, () => {
@@ -84,6 +87,10 @@ class MarkdownEditor extends React.Component {
this.refs.preview.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
+
+ const newConfig = Object.assign({}, config)
+ newConfig.editor.delfaultStatus = newStatus
+ ConfigManager.set(newConfig)
})
}
}
@@ -140,8 +147,10 @@ class MarkdownEditor extends React.Component {
e.preventDefault()
e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/
- const checkedMatch = /\[x\]/i
- const uncheckedMatch = /\[ \]/
+ const checkedMatch = /^\s*[\+\-\*] \[x\]/i
+ const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
+ const checkReplace = /\[x\]/i
+ const uncheckReplace = /\[ \]/
if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
const lines = this.refs.code.value
@@ -150,10 +159,10 @@ class MarkdownEditor extends React.Component {
const targetLine = lines[lineIndex]
if (targetLine.match(checkedMatch)) {
- lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
+ lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
}
if (targetLine.match(uncheckedMatch)) {
- lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
+ lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
}
this.refs.code.setValue(lines.join('\n'))
}
@@ -250,7 +259,7 @@ class MarkdownEditor extends React.Component {
: 'codeEditor--hide'
}
ref='code'
- mode='GitHub Flavored Markdown'
+ mode='Boost Flavored Markdown'
value={value}
theme={config.editor.theme}
keyMap={config.editor.keyMap}
@@ -268,6 +277,7 @@ class MarkdownEditor extends React.Component {
enableTableEditor={config.editor.enableTableEditor}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
+ spellCheck={config.editor.spellcheck}
/>
{percentageOfTodo}%
onClearCheckboxClick(e)}>clear
+${str}`
- }
- if (langType === 'sequence') {
- return `${str}`
- }
- if (langType === 'chart') {
- return `${str}`
- }
- if (langType === 'mermaid') {
- return `${str}`
- }
- return '' +
- '' + fileName + '' +
- createGutter(str, firstLineNumber) +
- '' +
- str +
- ''
- },
sanitize: 'STRICT'
}
@@ -105,7 +80,11 @@ class Markdown {
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
'input': ['type', 'id', 'checked']
},
- allowedIframeHostnames: ['www.youtube.com']
+ allowedIframeHostnames: ['www.youtube.com'],
+ selfClosing: [ 'img', 'br', 'hr', 'input' ],
+ allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
+ allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
+ allowProtocolRelative: true
})
}
@@ -139,19 +118,60 @@ class Markdown {
this.md.use(require('markdown-it-imsize'))
this.md.use(require('markdown-it-footnote'))
this.md.use(require('markdown-it-multimd-table'))
- this.md.use(require('markdown-it-named-headers'), {
- slugify: (header) => {
- return encodeURI(header.trim()
+ this.md.use(anchor, {
+ slugify: (title) => {
+ var slug = encodeURI(title.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
+ return slug
}
})
this.md.use(require('markdown-it-kbd'))
-
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
+ this.md.use(require('markdown-it-abbr'))
+ this.md.use(require('markdown-it-sub'))
+ this.md.use(require('markdown-it-sup'))
+ this.md.use(require('./markdown-it-deflist'))
this.md.use(require('./markdown-it-frontmatter'))
+ this.md.use(require('./markdown-it-fence'), {
+ chart: token => {
+ if (token.parameters.hasOwnProperty('yaml')) {
+ token.parameters.format = 'yaml'
+ }
+
+ return `+ ${token.fileName} +` + }, + flowchart: token => { + return `${token.content}+
+ ${token.fileName} +` + }, + mermaid: token => { + return `${token.content}+
+ ${token.fileName} +` + }, + sequence: token => { + return `${token.content}+
+ ${token.fileName} +` + } + }, token => { + return `${token.content}+
+ ${token.fileName}
+ ${createGutter(token.content, token.firstLineNumber)}
+ ${token.content}
+ `
+ })
+
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) {
@@ -223,7 +243,11 @@ class Markdown {
if (!liToken.attrs) {
liToken.attrs = []
}
- liToken.attrs.push(['class', 'taskListItem'])
+ if (config.preview.lineThroughCheckbox) {
+ liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`])
+ } else {
+ liToken.attrs.push(['class', 'taskListItem'])
+ }
}
content = ``
}
@@ -248,9 +272,12 @@ class Markdown {
this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => {
switch (token.type) {
- case 'heading_open':
- case 'paragraph_open':
case 'blockquote_open':
+ case 'dd_open':
+ case 'dt_open':
+ case 'heading_open':
+ case 'list_item_open':
+ case 'paragraph_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
diff --git a/browser/lib/newNote.js b/browser/lib/newNote.js
index bed69735..0b64d0e1 100644
--- a/browser/lib/newNote.js
+++ b/browser/lib/newNote.js
@@ -3,14 +3,21 @@ import dataApi from 'browser/main/lib/dataApi'
import ee from 'browser/main/lib/eventEmitter'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
-export function createMarkdownNote (storage, folder, dispatch, location) {
+export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
+
+ let tags = []
+ if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
+ tags = params.tagname.split(' ')
+ }
+
return dataApi
.createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
+ tags,
content: ''
})
.then(note => {
@@ -29,14 +36,21 @@ export function createMarkdownNote (storage, folder, dispatch, location) {
})
}
-export function createSnippetNote (storage, folder, dispatch, location, config) {
+export function createSnippetNote (storage, folder, dispatch, location, params, config) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
+
+ let tags = []
+ if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
+ tags = params.tagname.split(' ')
+ }
+
return dataApi
.createNote(storage, {
type: 'SNIPPET_NOTE',
folder: folder,
title: '',
+ tags,
description: '',
snippets: [
{
diff --git a/browser/lib/spellcheck.js b/browser/lib/spellcheck.js
new file mode 100644
index 00000000..dd04e575
--- /dev/null
+++ b/browser/lib/spellcheck.js
@@ -0,0 +1,232 @@
+import styles from '../components/CodeEditor.styl'
+import i18n from 'browser/lib/i18n'
+
+const Typo = require('typo-js')
+const _ = require('lodash')
+
+const CSS_ERROR_CLASS = 'codeEditor-typo'
+const SPELLCHECK_DISABLED = 'NONE'
+const DICTIONARY_PATH = '../dictionaries'
+const MILLISECONDS_TILL_LIVECHECK = 500
+
+let dictionary = null
+let self
+
+function getAvailableDictionaries () {
+ return [
+ {label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED},
+ {label: i18n.__('English'), value: 'en_GB'},
+ {label: i18n.__('German'), value: 'de_DE'},
+ {label: i18n.__('French'), value: 'fr_FR'}
+ ]
+}
+
+/**
+ * Only to be used in the tests :)
+ */
+function setDictionaryForTestsOnly (newDictionary) {
+ dictionary = newDictionary
+}
+
+/**
+ * @description Initializes the spellcheck. It removes all existing marks of the current editor.
+ * If a language was given (i.e. lang !== this.SPELLCHECK_DISABLED) it will load the stated dictionary and use it to check the whole document.
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param {String} lang on of the values from getAvailableDictionaries()-Method
+ */
+function setLanguage (editor, lang) {
+ self = this
+ dictionary = null
+
+ if (editor == null) {
+ return
+ }
+
+ const existingMarks = editor.getAllMarks() || []
+ for (const mark of existingMarks) {
+ mark.clear()
+ }
+ if (lang !== SPELLCHECK_DISABLED) {
+ dictionary = new Typo(lang, false, false, {
+ dictionaryPath: DICTIONARY_PATH,
+ asyncLoad: true,
+ loadedCallback: () =>
+ checkWholeDocument(editor)
+ })
+ }
+}
+
+/**
+ * Checks the whole content of the editor for typos
+ * @param {Codemirror} editor CodeMirror-Editor
+ */
+function checkWholeDocument (editor) {
+ const lastLine = editor.lineCount() - 1
+ const textOfLastLine = editor.getLine(lastLine) || ''
+ const lastChar = textOfLastLine.length
+ const from = {line: 0, ch: 0}
+ const to = {line: lastLine, ch: lastChar}
+ checkMultiLineRange(editor, from, to)
+}
+
+/**
+ * Checks the given range for typos
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param {line, ch} from starting position of the spellcheck
+ * @param {line, ch} to end position of the spellcheck
+ */
+function checkMultiLineRange (editor, from, to) {
+ function sortRange (pos1, pos2) {
+ if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) {
+ return {from: pos2, to: pos1}
+ }
+ return {from: pos1, to: pos2}
+ }
+
+ const {from: smallerPos, to: higherPos} = sortRange(from, to)
+ for (let l = smallerPos.line; l <= higherPos.line; l++) {
+ const line = editor.getLine(l) || ''
+ let w = 0
+ if (l === smallerPos.line) {
+ w = smallerPos.ch
+ }
+ let wEnd = line.length
+ if (l === higherPos.line) {
+ wEnd = higherPos.ch
+ }
+ while (w <= wEnd) {
+ const wordRange = editor.findWordAt({line: l, ch: w})
+ self.checkWord(editor, wordRange)
+ w += (wordRange.head.ch - wordRange.anchor.ch) + 1
+ }
+ }
+}
+
+/**
+ * @description Checks whether a certain range of characters in the editor (i.e. a word) contains a typo.
+ * If so the ranged will be marked with the class CSS_ERROR_CLASS.
+ * Note: Due to performance considerations, only words with more then 3 signs are checked.
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param wordRange Object specifying the range that should be checked.
+ * Having the following structure: {anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}
+ */
+function checkWord (editor, wordRange) {
+ const word = editor.getRange(wordRange.anchor, wordRange.head)
+ if (word == null || word.length <= 3) {
+ return
+ }
+ if (!dictionary.check(word)) {
+ editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]})
+ }
+}
+
+/**
+ * Checks the changes recently made (aka live check)
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param fromChangeObject codeMirror changeObject describing the start of the editing
+ * @param toChangeObject codeMirror changeObject describing the end of the editing
+ */
+function checkChangeRange (editor, fromChangeObject, toChangeObject) {
+ /**
+ * Calculate the smallest respectively largest position as a start, resp. end, position and return it
+ * @param start CodeMirror change object
+ * @param end CodeMirror change object
+ * @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
+ */
+ function getStartAndEnd (start, end) {
+ const possiblePositions = [start.from, start.to, end.from, end.to]
+ let smallest = start.from
+ let biggest = end.to
+ for (const currentPos of possiblePositions) {
+ if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) {
+ smallest = currentPos
+ }
+ if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) {
+ biggest = currentPos
+ }
+ }
+ return {start: smallest, end: biggest}
+ }
+
+ if (dictionary === null || editor == null) { return }
+
+ try {
+ const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject)
+
+ // Expand the range to include words after/before whitespaces
+ start.ch = Math.max(start.ch - 1, 0)
+ end.ch = end.ch + 1
+
+ // clean existing marks
+ const existingMarks = editor.findMarks(start, end) || []
+ for (const mark of existingMarks) {
+ mark.clear()
+ }
+
+ self.checkMultiLineRange(editor, start, end)
+ } catch (e) {
+ console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', e)
+ }
+}
+
+function saveLiveSpellCheckFrom (changeObject) {
+ liveSpellCheckFrom = changeObject
+}
+let liveSpellCheckFrom
+const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, {
+ 'leading': true,
+ 'trailing': false
+})
+const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, {
+ 'leading': false,
+ 'trailing': true
+})
+
+/**
+ * Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param changeObject codeMirror changeObject
+ */
+function handleChange (editor, changeObject) {
+ if (dictionary === null) {
+ return
+ }
+ debouncedSpellCheckLeading(changeObject)
+ debouncedSpellCheck(editor, liveSpellCheckFrom, changeObject)
+}
+
+/**
+ * Returns an array of spelling suggestions for the given (wrong written) word.
+ * Returns an empty array if the dictionary is null (=> spellcheck is disabled) or the given word was null
+ * @param word word to be checked
+ * @returns {String[]} Array of suggestions
+ */
+function getSpellingSuggestion (word) {
+ if (dictionary == null || word == null) {
+ return []
+ }
+ return dictionary.suggest(word)
+}
+
+/**
+ * Returns the name of the CSS class used for errors
+ */
+function getCSSClassName () {
+ return styles[CSS_ERROR_CLASS]
+}
+
+module.exports = {
+ DICTIONARY_PATH,
+ CSS_ERROR_CLASS,
+ SPELLCHECK_DISABLED,
+ getAvailableDictionaries,
+ setLanguage,
+ checkChangeRange,
+ handleChange,
+ getSpellingSuggestion,
+ checkWord,
+ checkMultiLineRange,
+ checkWholeDocument,
+ setDictionaryForTestsOnly,
+ getCSSClassName
+}
diff --git a/browser/main/Detail/Detail.styl b/browser/main/Detail/Detail.styl
index 49a634f3..1b7bd606 100644
--- a/browser/main/Detail/Detail.styl
+++ b/browser/main/Detail/Detail.styl
@@ -23,7 +23,7 @@ body[data-theme="dark"]
border-left 1px solid $ui-dark-borderColor
.empty-message
color $ui-dark-inactive-text-color
-
+
body[data-theme="solarized-dark"]
.root
background-color $ui-solarized-dark-noteDetail-backgroundColor
@@ -37,3 +37,10 @@ body[data-theme="monokai"]
border-left 1px solid $ui-monokai-borderColor
.empty-message
color $ui-monokai-text-color
+
+body[data-theme="dracula"]
+ .root
+ background-color $ui-dracula-noteDetail-backgroundColor
+ border-left 1px solid $ui-dracula-borderColor
+ .empty-message
+ color $ui-dracula-text-color
\ No newline at end of file
diff --git a/browser/main/Detail/FolderSelect.styl b/browser/main/Detail/FolderSelect.styl
index cfdc2734..fe045e3a 100644
--- a/browser/main/Detail/FolderSelect.styl
+++ b/browser/main/Detail/FolderSelect.styl
@@ -36,7 +36,7 @@
height 34px
width 20px
line-height 34px
-
+
.search-input
vertical-align middle
position relative
@@ -71,7 +71,7 @@
overflow ellipsis
cursor pointer
&:hover
- background-color $ui-button--hover-backgroundColor
+ background-color $ui-button--hover-backgroundColor
.search-optionList-item--active
@extend .search-optionList-item
@@ -159,3 +159,29 @@ body[data-theme="monokai"]
color $ui-monokai-button--active-color
.search-optionList-item-name-surfix
color $ui-monokai-inactive-text-color
+
+body[data-theme="dracula"]
+ .root
+ color $ui-dracula-text-color
+ &:hover
+ color #f8f8f2
+ background-color $ui-dark-button--hover-backgroundColor
+ border-color $ui-dracula-borderColor
+
+ .search-optionList
+ color #f8f8f2
+ border-color $ui-dracula-borderColor
+ background-color $ui-dracula-button-backgroundColor
+
+ .search-optionList-item
+ &:hover
+ background-color lighten($ui-dracula-button--hover-backgroundColor, 15%)
+
+ .search-optionList-item--active
+ background-color $ui-dracula-button--active-backgroundColor
+ color $ui-dracula-button--active-color
+ &:hover
+ background-color $ui-dark-button--hover-backgroundColor
+ color $ui-dracula-button--active-color
+ .search-optionList-item-name-surfix
+ color $ui-dracula-inactive-text-color
diff --git a/browser/main/Detail/InfoPanel.styl b/browser/main/Detail/InfoPanel.styl
index 2a73ca7e..1f774174 100644
--- a/browser/main/Detail/InfoPanel.styl
+++ b/browser/main/Detail/InfoPanel.styl
@@ -257,3 +257,43 @@ body[data-theme="monokai"]
color $ui-dark-inactive-text-color
&:hover
color $ui-monokai-text-color
+
+body[data-theme="dracula"]
+ .control-infoButton-panel
+ background-color $ui-dracula-noteList-backgroundColor
+
+ .control-infoButton-panel-trash
+ background-color $ui-dracula-noteList-backgroundColor
+
+ .modification-date
+ color $ui-dracula-text-color
+
+ .modification-date-desc
+ color $ui-inactive-text-color
+
+ .infoPanel-defaul-count
+ color $ui-dracula-text-color
+
+ .infoPanel-sub-count
+ color $ui-inactive-text-color
+
+ .infoPanel-default
+ color $ui-dracula-text-color
+
+ .infoPanel-sub
+ color $ui-inactive-text-color
+
+ .infoPanel-noteLink
+ background-color alpha($ui-dracula-borderColor, 20%)
+ color $ui-dracula-text-color
+
+ [id=export-wrap]
+ button
+ color $ui-dark-inactive-text-color
+ &:hover
+ background-color alpha($ui-dracula-borderColor, 20%)
+ color $ui-dracula-text-color
+ p
+ color $ui-dark-inactive-text-color
+ &:hover
+ color $ui-dracula-text-color
\ No newline at end of file
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index e4493a80..b4e7a5b3 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -61,11 +61,14 @@ class MarkdownNoteDetail extends React.Component {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType)
})
+ ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
ee.on('code:generate-toc', this.generateToc)
}
componentWillReceiveProps (nextProps) {
- if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
+ const isNewNote = nextProps.note.key !== this.props.note.key
+ const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length
+ if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow()
this.setState({
note: Object.assign({}, nextProps.note)
@@ -91,7 +94,7 @@ class MarkdownNoteDetail extends React.Component {
handleUpdateContent () {
const { note } = this.state
note.content = this.refs.content.value
- note.title = markdown.strip(striptags(findNoteTitle(note.content)))
+ note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
this.updateNote(note)
}
@@ -187,6 +190,36 @@ class MarkdownNoteDetail extends React.Component {
ee.emit('export:save-html')
}
+ handleKeyDown (e) {
+ switch (e.keyCode) {
+ // tab key
+ case 9:
+ if (e.ctrlKey && !e.shiftKey) {
+ e.preventDefault()
+ this.jumpNextTab()
+ } else if (e.ctrlKey && e.shiftKey) {
+ e.preventDefault()
+ this.jumpPrevTab()
+ } else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
+ e.preventDefault()
+ this.focusEditor()
+ }
+ break
+ // I key
+ case 73:
+ {
+ const isSuper = global.process.platform === 'darwin'
+ ? e.metaKey
+ : e.ctrlKey
+ if (isSuper) {
+ e.preventDefault()
+ this.handleInfoButtonClick(e)
+ }
+ }
+ break
+ }
+ }
+
handleTrashButtonClick (e) {
const { note } = this.state
const { isTrashed } = note
@@ -293,9 +326,33 @@ class MarkdownNoteDetail extends React.Component {
})
}
+ handleDeleteNote () {
+ this.handleTrashButtonClick()
+ }
+
+ handleClearTodo () {
+ const { note } = this.state
+ const splitted = note.content.split('\n')
+
+ const clearTodoContent = splitted.map((line) => {
+ const trimmedLine = line.trim()
+ if (trimmedLine.match(/\[x\]/i)) {
+ return line.replace(/\[x\]/i, '[ ]')
+ } else {
+ return line
+ }
+ }).join('\n')
+
+ note.content = clearTodoContent
+ this.refs.content.setValue(note.content)
+
+ this.updateNote(note)
+ }
+
renderEditor () {
const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state
+
if (this.state.editorType === 'EDITOR_PREVIEW') {
return {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')}