mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
Merge branch 'master' into bug-2581
This commit is contained in:
@@ -11,9 +11,12 @@ 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'
|
||||
|
||||
@@ -30,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)
|
||||
}
|
||||
@@ -62,6 +65,12 @@ export default class CodeEditor extends React.Component {
|
||||
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()
|
||||
@@ -232,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')
|
||||
@@ -248,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({
|
||||
@@ -317,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,
|
||||
@@ -389,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)
|
||||
}
|
||||
|
||||
@@ -459,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()
|
||||
}
|
||||
@@ -472,10 +504,11 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,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 = {
|
||||
@@ -722,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 = {
|
||||
@@ -732,5 +785,6 @@ CodeEditor.defaultProps = {
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space'
|
||||
indentType: 'space',
|
||||
spellCheck: false
|
||||
}
|
||||
|
||||
6
browser/components/CodeEditor.styl
Normal file
6
browser/components/CodeEditor.styl
Normal file
@@ -0,0 +1,6 @@
|
||||
.codeEditor-typo
|
||||
text-decoration underline wavy red
|
||||
|
||||
.spellcheck-select
|
||||
border: none
|
||||
text-decoration underline wavy red
|
||||
@@ -147,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
|
||||
@@ -157,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'))
|
||||
}
|
||||
@@ -275,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}
|
||||
/>
|
||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||
? 'preview'
|
||||
|
||||
@@ -21,6 +21,7 @@ import yaml from 'js-yaml'
|
||||
import context from 'browser/lib/context'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import fs from 'fs'
|
||||
import ConfigManager from '../main/lib/ConfigManager'
|
||||
|
||||
const { remote, shell } = require('electron')
|
||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||
@@ -263,6 +264,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
const config = ConfigManager.get()
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
||||
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
||||
}
|
||||
if (e.target != null) {
|
||||
switch (e.target.tagName) {
|
||||
case 'A':
|
||||
|
||||
@@ -78,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
|
||||
@@ -88,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'))
|
||||
}
|
||||
@@ -169,6 +171,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
noteKey={noteKey}
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
spellCheck={config.editor.spellcheck}
|
||||
/>
|
||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||
<div styleName='slider-hitbox' />
|
||||
|
||||
@@ -25,7 +25,7 @@ function render (element, content, theme) {
|
||||
if (height && height.value !== 'undefined') {
|
||||
element.style.height = height.value + 'vh'
|
||||
}
|
||||
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
|
||||
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
|
||||
mermaidAPI.initialize({
|
||||
theme: isDarkTheme ? 'dark' : 'default',
|
||||
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
||||
|
||||
65
browser/lib/contextMenuBuilder.js
Normal file
65
browser/lib/contextMenuBuilder.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const {remote} = require('electron')
|
||||
const {Menu} = remote.require('electron')
|
||||
const spellcheck = require('./spellcheck')
|
||||
|
||||
/**
|
||||
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
|
||||
* If the word is does not contains a spelling error (determined by the 'error style'), no suggestions for corrections are requested
|
||||
* => they are not visible in the context menu
|
||||
* @param editor CodeMirror editor
|
||||
* @param {MouseEvent} event that has triggered the creation of the context menu
|
||||
* @returns {Electron.Menu} The created electron context menu
|
||||
*/
|
||||
const buildEditorContextMenu = function (editor, event) {
|
||||
if (editor == null || event == null || event.pageX == null || event.pageY == null) {
|
||||
return null
|
||||
}
|
||||
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY})
|
||||
const wordRange = editor.findWordAt(cursor)
|
||||
const word = editor.getRange(wordRange.anchor, wordRange.head)
|
||||
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
|
||||
let isMisspelled = false
|
||||
for (const mark of existingMarks) {
|
||||
if (mark.className === spellcheck.getCSSClassName()) {
|
||||
isMisspelled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
let suggestion = []
|
||||
if (isMisspelled) {
|
||||
suggestion = spellcheck.getSpellingSuggestion(word)
|
||||
}
|
||||
|
||||
const selection = {
|
||||
isMisspelled: isMisspelled,
|
||||
spellingSuggestions: suggestion
|
||||
}
|
||||
const template = [{
|
||||
role: 'cut'
|
||||
}, {
|
||||
role: 'copy'
|
||||
}, {
|
||||
role: 'paste'
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}]
|
||||
|
||||
if (selection.isMisspelled) {
|
||||
const suggestions = selection.spellingSuggestions
|
||||
template.unshift.apply(template, suggestions.map(function (suggestion) {
|
||||
return {
|
||||
label: suggestion,
|
||||
click: function (suggestion) {
|
||||
if (editor != null) {
|
||||
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).concat({
|
||||
type: 'separator'
|
||||
}))
|
||||
}
|
||||
return Menu.buildFromTemplate(template)
|
||||
}
|
||||
|
||||
module.exports = buildEditorContextMenu
|
||||
@@ -3,7 +3,7 @@
|
||||
module.exports = function (md, renderers, defaultRenderer) {
|
||||
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
|
||||
|
||||
function fence (state, startLine, endLine) {
|
||||
function fence (state, startLine, endLine, silent) {
|
||||
let pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||
let max = state.eMarks[startLine]
|
||||
|
||||
@@ -12,7 +12,7 @@ module.exports = function (md, renderers, defaultRenderer) {
|
||||
}
|
||||
|
||||
const marker = state.src.charCodeAt(pos)
|
||||
if (!(marker === 96 || marker === 126)) {
|
||||
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ module.exports = function (md, renderers, defaultRenderer) {
|
||||
const markup = state.src.slice(mem, pos)
|
||||
const params = state.src.slice(pos, max)
|
||||
|
||||
if (silent) {
|
||||
return true
|
||||
}
|
||||
|
||||
let nextLine = startLine
|
||||
let haveEndMarker = false
|
||||
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
232
browser/lib/spellcheck.js
Normal file
232
browser/lib/spellcheck.js
Normal file
@@ -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: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
|
||||
*/
|
||||
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
|
||||
}
|
||||
@@ -190,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
|
||||
@@ -458,6 +488,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
@@ -434,6 +434,18 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.focusEditor()
|
||||
}
|
||||
break
|
||||
// I key
|
||||
case 73:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
e.preventDefault()
|
||||
this.handleInfoButtonClick(e)
|
||||
}
|
||||
}
|
||||
break
|
||||
// L key
|
||||
case 76:
|
||||
{
|
||||
|
||||
@@ -167,6 +167,8 @@ class Main extends React.Component {
|
||||
}
|
||||
})
|
||||
|
||||
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
||||
|
||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,19 +35,20 @@ class NewNoteButton extends React.Component {
|
||||
}
|
||||
|
||||
handleNewNoteButtonClick (e) {
|
||||
const { location, dispatch, config } = this.props
|
||||
const { location, params, dispatch, config } = this.props
|
||||
const { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||
createMarkdownNote(storage.key, folder.key, dispatch, location)
|
||||
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
||||
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||
createSnippetNote(storage.key, folder.key, dispatch, location, config)
|
||||
createSnippetNote(storage.key, folder.key, dispatch, location, params, config)
|
||||
} else {
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location,
|
||||
params,
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,7 +83,9 @@ class NoteList extends React.Component {
|
||||
|
||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||
this.state = {
|
||||
ctrlKeyDown: false,
|
||||
shiftKeyDown: false,
|
||||
prevShiftNoteIndex: -1,
|
||||
selectedNoteKeys: []
|
||||
}
|
||||
|
||||
@@ -266,7 +268,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
handleNoteListKeyDown (e) {
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
if (e.metaKey) return true
|
||||
|
||||
// A key
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
@@ -306,6 +308,8 @@ class NoteList extends React.Component {
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: true })
|
||||
} else if (e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +317,10 @@ class NoteList extends React.Component {
|
||||
if (!e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: false })
|
||||
}
|
||||
|
||||
if (!e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: false })
|
||||
}
|
||||
}
|
||||
|
||||
getNotes () {
|
||||
@@ -389,25 +397,65 @@ class NoteList extends React.Component {
|
||||
return pinnedNotes.concat(unpinnedNotes)
|
||||
}
|
||||
|
||||
getNoteIndexByKey (noteKey) {
|
||||
return this.notes.findIndex((note) => {
|
||||
if (!note) return -1
|
||||
|
||||
return note.key === noteKey
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
||||
const { ctrlKeyDown, shiftKeyDown } = this.state
|
||||
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||
|
||||
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
||||
this.setState({
|
||||
selectedNoteKeys: newSelectedNoteKeys
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!shiftKeyDown) {
|
||||
if (!ctrlKeyDown && !shiftKeyDown) {
|
||||
selectedNoteKeys = []
|
||||
}
|
||||
|
||||
if (!shiftKeyDown) {
|
||||
prevShiftNoteIndex = -1
|
||||
}
|
||||
|
||||
selectedNoteKeys.push(uniqueKey)
|
||||
|
||||
if (shiftKeyDown && hasSelectedNoteKey) {
|
||||
let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0])
|
||||
// Shift selection can either start from first note in the exisiting selectedNoteKeys
|
||||
// or previous first shift note index
|
||||
firstShiftNoteIndex = firstShiftNoteIndex > prevShiftNoteIndex
|
||||
? firstShiftNoteIndex : prevShiftNoteIndex
|
||||
|
||||
const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey)
|
||||
|
||||
const startIndex = firstShiftNoteIndex < lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
const endIndex = firstShiftNoteIndex > lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
|
||||
selectedNoteKeys = []
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
selectedNoteKeys.push(this.notes[i].key)
|
||||
}
|
||||
|
||||
if (prevShiftNoteIndex < 0) {
|
||||
prevShiftNoteIndex = firstShiftNoteIndex
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys
|
||||
selectedNoteKeys,
|
||||
prevShiftNoteIndex
|
||||
})
|
||||
|
||||
router.push({
|
||||
|
||||
@@ -274,7 +274,7 @@ class StorageItem extends React.Component {
|
||||
const { folderNoteMap, trashedSet } = data
|
||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||
const folderList = storage.folders.map((folder, index) => {
|
||||
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||
const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||
const isActive = !!(location.pathname.match(folderRegex))
|
||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {SortableContainer} from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import context from 'browser/lib/context'
|
||||
import { remote } from 'electron'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
|
||||
function matchActiveTags (tags, activeTags) {
|
||||
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
||||
@@ -269,7 +270,7 @@ class SideNav extends React.Component {
|
||||
const tags = pathSegments[pathSegments.length - 1]
|
||||
return (tags === 'alltags')
|
||||
? []
|
||||
: tags.split(' ').map(tag => decodeURIComponent(tag))
|
||||
: decodeURIComponent(tags).split(' ')
|
||||
}
|
||||
|
||||
handleClickTagListItem (name) {
|
||||
@@ -301,7 +302,7 @@ class SideNav extends React.Component {
|
||||
} else {
|
||||
listOfTags.push(tag)
|
||||
}
|
||||
router.push(`/tags/${listOfTags.map(tag => encodeURIComponent(tag)).join(' ')}`)
|
||||
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
|
||||
}
|
||||
|
||||
emptyTrash (entries) {
|
||||
@@ -309,6 +310,8 @@ class SideNav extends React.Component {
|
||||
const deletionPromises = entries.map((note) => {
|
||||
return dataApi.deleteNote(note.storage, note.key)
|
||||
})
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||
Promise.all(deletionPromises)
|
||||
.then((arrayOfStorageAndNoteKeys) => {
|
||||
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ export const DEFAULT_CONFIG = {
|
||||
fetchUrlTitle: true,
|
||||
enableTableEditor: false,
|
||||
enableFrontMatterTitle: true,
|
||||
frontMatterTitleField: 'title'
|
||||
frontMatterTitleField: 'title',
|
||||
spellcheck: false
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ class UiTab extends React.Component {
|
||||
theme: this.refs.uiTheme.value,
|
||||
language: this.refs.uiLanguage.value,
|
||||
defaultNote: this.refs.defaultNote.value,
|
||||
tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags.checked,
|
||||
showCopyNotification: this.refs.showCopyNotification.checked,
|
||||
confirmDeletion: this.refs.confirmDeletion.checked,
|
||||
showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
|
||||
@@ -94,7 +95,8 @@ class UiTab extends React.Component {
|
||||
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked,
|
||||
enableTableEditor: this.refs.enableTableEditor.checked,
|
||||
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
|
||||
frontMatterTitleField: this.refs.frontMatterTitleField.value
|
||||
frontMatterTitleField: this.refs.frontMatterTitleField.value,
|
||||
spellcheck: this.refs.spellcheck.checked
|
||||
},
|
||||
preview: {
|
||||
fontSize: this.refs.previewFontSize.value,
|
||||
@@ -267,6 +269,7 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
<div styleName='group-header2'>Tags</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
@@ -313,6 +316,17 @@ class UiTab extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.tagNewNoteWithFilteringTags}
|
||||
ref='tagNewNoteWithFilteringTags'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('New notes are tagged with the filtering tags')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-header2'>Editor</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
@@ -538,6 +552,16 @@ class UiTab extends React.Component {
|
||||
{i18n.__('Enable smart table editor')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.spellcheck}
|
||||
ref='spellcheck'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Enable spellcheck - Experimental feature!! :)')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-header2'>{i18n.__('Preview')}</div>
|
||||
<div styleName='group-section'>
|
||||
|
||||
Reference in New Issue
Block a user