1
0
mirror of https://github.com/BoostIo/Boostnote synced 2026-01-09 06:59:20 +00:00

Merge branch 'master' into fix-scroll

This commit is contained in:
Baptiste Augrain
2018-11-28 14:01:18 +01:00
142 changed files with 223650 additions and 787 deletions

View File

@@ -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) {
@@ -129,6 +143,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 = [
{
@@ -230,6 +245,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')
@@ -247,6 +263,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({
@@ -296,6 +316,10 @@ export default class CodeEditor extends React.Component {
if (this.props.enableTableEditor) {
this.editor.on('changes', this.editorActivityHandler)
}
this.setState({
clientWidth: this.refs.root.clientWidth
})
}
expandSnippet (line, cursor, cm, snippets) {
@@ -311,22 +335,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,
@@ -384,10 +414,12 @@ export default class CodeEditor extends React.Component {
eventEmitter.off('top:search', this.searchHandler)
this.editor.off('scroll', this.scrollHandler)
this.editor.off('cursorActivity', this.editorActivityHandler)
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)
if (this.props.enableTableEditor) {
@@ -451,6 +483,24 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('extraKeys', this.defaultKeyMap)
}
if (this.state.clientWidth !== this.refs.root.clientWidth) {
this.setState({
clientWidth: this.refs.root.clientWidth
})
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()
}
@@ -464,16 +514,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()
@@ -536,7 +593,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,
@@ -606,6 +667,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) => {
@@ -688,6 +755,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 = {
@@ -698,7 +784,8 @@ CodeEditor.propTypes = {
className: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
readOnly: PropTypes.bool
readOnly: PropTypes.bool,
spellCheck: PropTypes.bool
}
CodeEditor.defaultProps = {
@@ -708,5 +795,6 @@ CodeEditor.defaultProps = {
fontSize: 14,
fontFamily: 'Monaco, Consolas',
indentSize: 4,
indentType: 'space'
indentType: 'space',
spellCheck: false
}

View File

@@ -0,0 +1,6 @@
.codeEditor-typo
text-decoration underline wavy red
.spellcheck-select
border: none
text-decoration underline wavy red

View File

@@ -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}
/>
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview'
@@ -300,6 +310,7 @@ class MarkdownEditor extends React.Component {
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
/>
</div>
)

View File

@@ -16,7 +16,6 @@
.preview
display block
absolute top bottom left right
z-index 100
background-color white
height 100%
width 100%

View File

@@ -17,8 +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 ConfigManager from '../main/lib/ConfigManager'
const { remote } = require('electron')
const { remote, shell } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const { app } = remote
@@ -27,6 +32,8 @@ const fileUrl = require('file-url')
const dialog = remote.dialog
const uri2path = require('file-uri-to-path')
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = fileUrl(
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
@@ -75,7 +82,6 @@ function buildStyle (
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${allowCustomCSS ? customCSS : ''}
${markdownStyle}
body {
@@ -83,6 +89,11 @@ body {
font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'}
}
@media print {
body {
padding-bottom: initial;
}
}
code {
font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04);
@@ -139,6 +150,8 @@ body p {
display: none
}
}
${allowCustomCSS ? customCSS : ''}
`
}
@@ -161,7 +174,6 @@ const scrollBarDarkStyle = `
}
`
const { shell } = require('electron')
const OSX = global.process.platform === 'darwin'
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
@@ -219,8 +231,32 @@ export default class MarkdownPreview extends React.Component {
}
}
handleContextMenu (e) {
this.props.onContextMenu(e)
handleContextMenu (event) {
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
if (_.isFunction(this.props.onContextMenu)) {
this.props.onContextMenu(event)
return
}
// No contextMenu was passed to us -> execute our own link-opener
if (event.target.tagName.toLowerCase() === 'a') {
const href = event.target.href
const isLocalFile = href.startsWith('file:')
if (isLocalFile) {
const absPath = uri2path(href)
try {
if (fs.lstatSync(absPath).isFile()) {
context.popup([
{
label: i18n.__('Show in explorer'),
click: (e) => shell.showItemInFolder(absPath)
}
])
}
} catch (e) {
console.log('Error while evaluating if the file is locally available', e)
}
}
}
}
handleDoubleClick (e) {
@@ -228,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':
@@ -297,9 +337,7 @@ export default class MarkdownPreview extends React.Component {
allowCustomCSS,
customCSS
)
let body = this.markdown.render(
escapeHtmlCharacters(noteContent, { detectCodeBlock: true })
)
let body = this.markdown.render(noteContent)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
@@ -397,6 +435,7 @@ export default class MarkdownPreview extends React.Component {
case 'dark':
case 'solarized-dark':
case 'monokai':
case 'dracula':
return scrollBarDarkStyle
default:
return scrollBarStyle
@@ -498,7 +537,8 @@ export default class MarkdownPreview extends React.Component {
prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.sanitize !== this.props.sanitize ||
prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks
prevProps.breaks !== this.props.breaks ||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
) {
this.initMarkdown()
this.rewriteIframe()
@@ -704,7 +744,6 @@ export default class MarkdownPreview extends React.Component {
el.addEventListener('click', this.linkClickHandler)
})
} catch (e) {
console.error(e)
el.className = 'flowchart-error'
el.innerHTML = 'Flowchart parse error: ' + e.message
}
@@ -725,7 +764,6 @@ export default class MarkdownPreview extends React.Component {
el.addEventListener('click', this.linkClickHandler)
})
} catch (e) {
console.error(e)
el.className = 'sequence-error'
el.innerHTML = 'Sequence diagram parse error: ' + e.message
}
@@ -736,14 +774,21 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
el => {
try {
const chartConfig = JSON.parse(el.innerHTML)
const format = el.attributes.getNamedItem('data-format').value
const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
el.innerHTML = ''
var canvas = document.createElement('canvas')
const canvas = document.createElement('canvas')
el.appendChild(canvas)
/* eslint-disable no-new */
new Chart(canvas, chartConfig)
const height = el.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') {
el.style.height = height.value + 'vh'
canvas.height = height.value + 'vh'
}
const chart = new Chart(canvas, chartConfig)
} catch (e) {
console.error(e)
el.className = 'chart-error'
el.innerHTML = 'chartjs diagram parse error: ' + e.message
}
@@ -827,6 +872,15 @@ export default class MarkdownPreview extends React.Component {
return
}
const regexIsLine = /^:line:[0-9]/
if (regexIsLine.test(linkHash)) {
const numberPattern = /\d+/g
const lineNumber = parseInt(linkHash.match(numberPattern)[0])
eventEmitter.emit('line:jump', lineNumber)
return
}
// this will match the old link format storage.key-note.key
// e.g.
// 877f99c3268608328037-1c211eb7dcb463de6490

View File

@@ -165,8 +165,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
@@ -175,10 +177,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'))
}
@@ -264,7 +266,7 @@ class MarkdownSplitEditor extends React.Component {
styleName='codeEditor'
ref='code'
width={this.state.codeEditorWidthInPercent + '%'}
mode='GitHub Flavored Markdown'
mode='Boost Flavored Markdown'
value={value}
theme={config.editor.theme}
keyMap={config.editor.keyMap}
@@ -283,6 +285,7 @@ class MarkdownSplitEditor extends React.Component {
onChange={this.handleOnChange.bind(this)}
onScroll={this.handleEditorScroll.bind(this)}
onCursorActivity={this.handleCursorActivity.bind(this)}
spellCheck={config.editor.spellcheck}
/>
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
<div styleName='slider-hitbox' />
@@ -312,6 +315,7 @@ class MarkdownSplitEditor extends React.Component {
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
/>
</div>
)

View File

@@ -24,16 +24,19 @@ const TagElement = ({ tagName }) => (
/**
* @description Tag element list component.
* @param {Array|null} tags
* @param {boolean} showTagsAlphabetically
* @return {React.Component}
*/
const TagElementList = tags => {
const TagElementList = (tags, showTagsAlphabetically) => {
if (!isArray(tags)) {
return []
}
const tagElements = tags.map(tag => TagElement({ tagName: tag }))
return tagElements
if (showTagsAlphabetically) {
return _.sortBy(tags).map(tag => TagElement({ tagName: tag }))
} else {
return tags.map(tag => TagElement({ tagName: tag }))
}
}
/**
@@ -55,7 +58,8 @@ const NoteItem = ({
pathname,
storageName,
folderName,
viewType
viewType,
showTagsAlphabetically
}) => (
<div
styleName={isActive ? 'item--active' : 'item'}
@@ -74,28 +78,26 @@ const NoteItem = ({
? note.title
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
</div>
{['ALL', 'STORAGE'].includes(viewType) &&
<div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div>
<div styleName='item-middle-app-meta'>
<div
title={
viewType === 'ALL'
? storageName
: viewType === 'STORAGE' ? folderName : null
}
styleName='item-middle-app-meta-label'
>
{viewType === 'ALL' && storageName}
{viewType === 'STORAGE' && folderName}
</div>
<div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div>
<div styleName='item-middle-app-meta'>
<div
title={
viewType === 'ALL'
? storageName
: viewType === 'STORAGE' ? folderName : null
}
styleName='item-middle-app-meta-label'
>
{viewType === 'ALL' && storageName}
{viewType === 'STORAGE' && folderName}
</div>
</div>}
</div>
</div>
<div styleName='item-bottom'>
<div styleName='item-bottom-tagList'>
{note.tags.length > 0
? TagElementList(note.tags)
? TagElementList(note.tags, showTagsAlphabetically)
: <span
style={{ fontStyle: 'italic', opacity: 0.5 }}
styleName='item-bottom-tagList-empty'

View File

@@ -368,13 +368,13 @@ body[data-theme="monokai"]
.item-title
.item-title-icon
.item-bottom-time
color $ui-monokai-text-color
color $ui-monokai-active-color
.item-bottom-tagList-item
background-color alpha(white, 10%)
color $ui-monokai-text-color
&:hover
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%)
color #c0392b
color #f92672
.item-bottom-tagList-item
background-color alpha(#fff, 20%)
@@ -394,3 +394,76 @@ body[data-theme="monokai"]
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle
body[data-theme="dracula"]
.root
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
.item
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
&:hover
transition 0.15s
// background-color alpha($ui-dracula-noteList-backgroundColor, 20%)
color $ui-dracula-text-color
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-dracula-noteList-backgroundColor, 20%)
color $ui-dracula-text-color
&:active
transition 0.15s
background-color $ui-dracula-noteList-backgroundColor
color $ui-dracula-text-color
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-dracula-noteList-backgroundColor, 10%)
color $ui-dracula-text-color
.item-wrapper
border-color alpha($ui-dracula-button-backgroundColor, 60%)
.item--active
border-color $ui-dracula-borderColor
background-color $ui-dracula-button-backgroundColor
.item-wrapper
border-color transparent
.item-title
.item-title-icon
.item-bottom-time
color $ui-dracula-active-color
.item-bottom-tagList-item
background-color alpha(#f8f8f2, 10%)
color $ui-dracula-text-color
&:hover
// background-color alpha($ui-dracula-button--active-backgroundColor, 60%)
color #ff79c6
.item-bottom-tagList-item
background-color alpha(#f8f8f2, 20%)
.item-title
color $ui-inactive-text-color
.item-title-icon
color $ui-inactive-text-color
.item-title-empty
color $ui-inactive-text-color
.item-bottom-tagList-item
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
color $ui-inactive-text-color
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle

View File

@@ -240,7 +240,7 @@ body[data-theme="monokai"]
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-solarized-dark-text-color
color $ui-monokai-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(#fff, 20%)
@@ -286,3 +286,67 @@ body[data-theme="monokai"]
.item-simple-right-storageName
padding-left 4px
opacity 0.4
body[data-theme="dracula"]
.root
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
.item-simple
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
&:hover
transition 0.15s
background-color alpha($ui-dracula-button-backgroundColor, 60%)
color $ui-dracula-text-color
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(#f8f8f2, 20%)
color $ui-dracula-text-color
&:active
transition 0.15s
background-color $ui-dracula-button--active-backgroundColor
color $ui-dracula-text-color
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(#f8f8f2, 10%)
color $ui-dracula-text-color
.item-simple--active
border-color $ui-dracula-borderColor
background-color $ui-dracula-button--active-backgroundColor
.item-simple-wrapper
border-color transparent
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
color $ui-dracula-text-color
.item-simple-bottom-tagList-item
background-color alpha(#f8f8f2, 10%)
color $ui-dracula-text-color
&:hover
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
color #c0392b
.item-simple-bottom-tagList-item
background-color alpha(#f8f8f2, 20%)
.item-simple-title
color $ui-dark-text-color
border-bottom $ui-dark-borderColor
.item-simple-right
float right
.item-simple-right-storageName
padding-left 4px
opacity 0.4

View File

@@ -51,4 +51,15 @@ body[data-theme="monokai"]
border none
background-color $ui-monokai-button-backgroundColor
&:hover
color #5CB85C
color #5CB85C
body[data-theme="dracula"]
.notification-area
background-color none
.notification-link
color $ui-dracula-text-color
border none
background-color $ui-dracula-button-backgroundColor
&:hover
color #ff79c6

View File

@@ -263,4 +263,46 @@ body[data-theme="monokai"]
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
.menu-button-label
color $ui-monokai-text-color
color $ui-monokai-text-color
body[data-theme="dracula"]
.menu-button
&:active
background-color $ui-dracula-noteList-backgroundColor
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.menu-button-label
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button-label
color $ui-dracula-text-color
.menu-button-star--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.menu-button-label
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button-label
color $ui-dracula-text-color
.menu-button-trash--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.menu-button-label
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button-label
color $ui-dracula-text-color

View File

@@ -180,4 +180,48 @@ body[data-theme="monokai"]
color $ui-monokai-text-color
.deleteButton
color alpha($ui-monokai-text-color, 30%)
color alpha($ui-monokai-text-color, 30%)
body[data-theme="dracula"]
.root
color $ui-dracula-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-dracula-noteDetail-backgroundColor
.deleteButton
color $ui-dracula-text-color
&:hover
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
&:active
color $ui-dracula-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-dracula-text-color
border-color $ui-dracula-borderColor
&:hover
background-color $ui-dracula-noteDetail-backgroundColor
.deleteButton
color $ui-dracula-text-color
&:hover
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
&:active
color $ui-dracula-text-color
background-color $ui-dark-button--active-backgroundColor
.button
border none
color $ui-dracula-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-noteDetail-backgroundColor
.input
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color
.deleteButton
color alpha($ui-dracula-text-color, 30%)

View File

@@ -156,4 +156,23 @@ body[data-theme="monokai"]
background-color $ui-monokai-button-backgroundColor
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
background-color $ui-monokai-button-backgroundColor
body[data-theme="dracula"]
.folderList-item
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
&:active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.folderList-item--active
@extend .folderList-item
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
&:active
background-color $ui-dracula-button-backgroundColor
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor

View File

@@ -7,18 +7,18 @@ import styles from './StorageList.styl'
import CSSModules from 'browser/lib/CSSModules'
/**
* @param {Array} storgaeList
* @param {Array} storageList
*/
const StorageList = ({storageList, isFolded}) => (
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
{storageList.length > 0 ? storageList : (
<div styleName='storgaeList-empty'>No storage mount.</div>
<div styleName='storageList-empty'>No storage mount.</div>
)}
</div>
)
StorageList.propTypes = {
storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired
storageList: PropTypes.arrayOf(PropTypes.element).isRequired
}
export default CSSModules(StorageList, styles)

View File

@@ -14,8 +14,8 @@ import CSSModules from 'browser/lib/CSSModules'
* @param {bool} isRelated
*/
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
<div styleName='tagList-itemContainer'>
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => (
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}>
{isRelated
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
@@ -25,7 +25,7 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isAc
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
<span styleName='tagList-item-name'>
{`# ${name}`}
<span styleName='tagList-item-count'>{count}</span>
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
</span>
</button>
</div>

View File

@@ -12,7 +12,7 @@ import styles from './TodoListPercentage.styl'
*/
const TodoListPercentage = ({
percentageOfTodo
percentageOfTodo, onClearCheckboxClick
}) => (
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
@@ -20,11 +20,15 @@ const TodoListPercentage = ({
<p styleName='percentageText'>{percentageOfTodo}%</p>
</div>
</div>
<div styleName='todoClear'>
<p styleName='todoClearText' onClick={(e) => onClearCheckboxClick(e)}>clear</p>
</div>
</div>
)
TodoListPercentage.propTypes = {
percentageOfTodo: PropTypes.number.isRequired
percentageOfTodo: PropTypes.number.isRequired,
onClearCheckboxClick: PropTypes.func.isRequired
}
export default CSSModules(TodoListPercentage, styles)

View File

@@ -1,4 +1,5 @@
.percentageBar
display: flex
position absolute
top 72px
right 0px
@@ -30,6 +31,20 @@
color #f4f4f4
font-weight 600
.todoClear
display flex
justify-content: flex-end
position absolute
z-index 120
width 100%
height 100%
padding 2px 10px
.todoClearText
color #f4f4f4
cursor pointer
font-weight 500
body[data-theme="dark"]
.percentageBar
background-color #444444
@@ -39,7 +54,10 @@ body[data-theme="dark"]
.percentageText
color $ui-dark-text-color
.todoClearText
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.percentageBar
background-color #002b36
@@ -50,12 +68,28 @@ body[data-theme="solarized-dark"]
.percentageText
color #fdf6e3
.todoClearText
color #fdf6e3
body[data-theme="monokai"]
.percentageBar
background-color #f92672
background-color: $ui-monokai-borderColor
.progressBar
background-color: #373831
background-color $ui-monokai-active-color
.percentageText
color #fdf6e3
color $ui-monokai-text-color
body[data-theme="dracula"]
.percentageBar
background-color $ui-dracula-borderColor
.progressBar
background-color: $ui-dracula-active-color
.percentageText
color $ui-dracula-text-color
.percentageText
color $ui-dracula-text-color

View File

@@ -80,6 +80,9 @@ li
&.checked
text-decoration line-through
opacity 0.5
&.taskListItem.checked
text-decoration line-through
opacity 0.5
div.math-rendered
text-align center
.math-failed
@@ -206,41 +209,39 @@ code
text-decoration none
margin-right 2px
pre
padding 0.5em !important
padding 0.5rem !important
border solid 1px #D1D1D1
border-radius 5px
overflow-x auto
margin 0 0 1em
margin 0 0 1rem
display flex
line-height 1.4em
&.flowchart, &.sequence, &.chart
display flex
justify-content center
background-color white
&.CodeMirror
height initial
flex-wrap wrap
&>code
flex 1
overflow-x auto
code
background-color inherit
margin 0
padding 0
border none
border-radius 0
&.CodeMirror
height initial
flex-wrap wrap
&>code
flex 1
overflow-x auto
&.mermaid svg
max-width 100% !important
&>span.filename
width 100%
border-radius: 5px 0px 0px 0px
margin -8px 100% 8px -8px
padding 0px 6px
margin -0.5rem 100% 0.5rem -0.5rem
padding 0.125rem 0.375rem
background-color #777;
color white
&:empty
display none
&>span.lineNumber
display none
font-size 1em
padding 0.5em 0
margin -0.5em 0.5em -0.5em -0.5em
padding 0.5rem 0
margin -0.5rem 0.5rem -0.5rem -0.5rem
border-right 1px solid
text-align right
border-top-left-radius 4px
@@ -361,7 +362,7 @@ for name, val in admonition_types
.admonition.{name}
@extend $admonition
border-left-color: val[color]
.admonition.{name}>.admonition-title
@extend $admonition-title
border-bottom-color: .1rem solid rgba(val[color], 0.2)
@@ -372,6 +373,49 @@ for name, val in admonition_types
color: val[color]
content: val[icon]
dl
margin 2rem 0
padding 0
display flex
width 100%
flex-wrap wrap
align-items flex-start
border-bottom 1px solid borderColor
background-color tableHeadBgColor
dt
border-top 1px solid borderColor
font-weight bold
text-align right
overflow hidden
flex-basis 20%
padding 0.4rem 0.9rem
box-sizing border-box
dd
border-top 1px solid borderColor
flex-basis 80%
padding 0.4rem 0.9rem
min-height 2.5rem
background-color $ui-noteDetail-backgroundColor
box-sizing border-box
dd + dd
margin-left 20%
pre.fence
flex-wrap wrap
.chart, .flowchart, .mermaid, .sequence
display flex
justify-content center
background-color white
max-width 100%
flex-grow 1
canvas, svg
max-width 100% !important
themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -422,6 +466,14 @@ body[data-theme="dark"]
kbd
background-color themeDarkBorder
color themeDarkText
dl
border-color themeDarkBorder
background-color themeDarkTableHead
dt
border-color themeDarkBorder
dd
border-color themeDarkBorder
background-color themeDarkPreview
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
@@ -449,6 +501,14 @@ body[data-theme="solarized-dark"]
border-color themeSolarizedDarkTableBorder
&:last-child
border-right solid 1px themeSolarizedDarkTableBorder
dl
border-color themeDarkBorder
background-color themeSolarizedDarkTableHead
dt
border-color themeDarkBorder
dd
border-color themeDarkBorder
background-color $ui-solarized-dark-noteDetail-backgroundColor
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
@@ -477,4 +537,49 @@ body[data-theme="monokai"]
&:last-child
border-right solid 1px themeMonokaiTableBorder
kbd
background-color themeDarkBackground
background-color themeDarkBackground
dl
border-color themeDarkBorder
background-color themeMonokaiTableHead
dt
border-color themeDarkBorder
dd
border-color themeDarkBorder
background-color $ui-monokai-noteDetail-backgroundColor
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
themeDraculaTableHead = themeDraculaTableEven
themeDraculaTableBorder = themeDarkBorder
body[data-theme="dracula"]
color $ui-dracula-text-color
border-color themeDarkBorder
background-color $ui-dracula-noteDetail-backgroundColor
table
thead
tr
background-color themeDraculaTableHead
th
border-color themeDraculaTableBorder
&:last-child
border-right solid 1px themeDraculaTableBorder
tbody
tr:nth-child(2n + 1)
background-color themeDraculaTableOdd
tr:nth-child(2n)
background-color themeDraculaTableEven
td
border-color themeDraculaTableBorder
&:last-child
border-right solid 1px themeDraculaTableBorder
kbd
background-color themeDarkBackground
dl
border-color themeDarkBorder
background-color themeDraculaTableHead
dt
border-color themeDarkBorder
dd
border-color themeDarkBorder
background-color $ui-dracula-noteDetail-backgroundColor

View File

@@ -2,8 +2,8 @@ import mermaidAPI from 'mermaid'
// fixes bad styling in the mermaid dark theme
const darkThemeStyling = `
.loopText tspan {
fill: white;
.loopText tspan {
fill: white;
}`
function getRandomInt (min, max) {
@@ -11,9 +11,9 @@ function getRandomInt (min, max) {
}
function getId () {
var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
var id = 'm-'
for (var i = 0; i < 7; i++) {
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let id = 'm-'
for (let i = 0; i < 7; i++) {
id += pool[getRandomInt(0, 16)]
}
return id
@@ -21,16 +21,20 @@ function getId () {
function render (element, content, theme) {
try {
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai'
const height = element.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') {
element.style.height = height.value + 'vh'
}
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
mermaidAPI.initialize({
theme: isDarkTheme ? 'dark' : 'default',
themeCSS: isDarkTheme ? darkThemeStyling : ''
themeCSS: isDarkTheme ? darkThemeStyling : '',
useMaxWidth: false
})
mermaidAPI.render(getId(), content, (svgGraph) => {
element.innerHTML = svgGraph
})
} catch (e) {
console.error(e)
element.className = 'mermaid-error'
element.innerHTML = 'mermaid diagram parse error: ' + e.message
}

View File

@@ -48,8 +48,12 @@ const languages = [
locale: 'pl'
},
{
name: 'Portuguese',
locale: 'pt'
name: 'Portuguese (PT-BR)',
locale: 'pt-BR'
},
{
name: 'Portuguese (PT-PT)',
locale: 'pt-PT'
},
{
name: 'Russian',
@@ -61,6 +65,9 @@ const languages = [
}, {
name: 'Turkish',
locale: 'tr'
}, {
name: 'Thai',
locale: 'th'
}
]

View 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

View File

@@ -1,4 +1,4 @@
export function findNoteTitle (value) {
export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') {
const splitted = value.split('\n')
let title = null
let isInsideCodeBlock = false
@@ -6,6 +6,11 @@ export function findNoteTitle (value) {
if (splitted[0] === '---') {
let line = 0
while (++line < splitted.length) {
if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) {
title = splitted[line].substring(frontMatterTitleField.length + 1).trim()
break
}
if (splitted[line] === '---') {
splitted.splice(0, line + 1)
@@ -14,17 +19,19 @@ export function findNoteTitle (value) {
}
}
splitted.some((line, index) => {
const trimmedLine = line.trim()
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
if (trimmedLine.match('```')) {
isInsideCodeBlock = !isInsideCodeBlock
}
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
title = trimmedLine
return true
}
})
if (title === null) {
splitted.some((line, index) => {
const trimmedLine = line.trim()
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
if (trimmedLine.match('```')) {
isInsideCodeBlock = !isInsideCodeBlock
}
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
title = trimmedLine
return true
}
})
}
if (title === null) {
title = ''

View File

@@ -0,0 +1,232 @@
'use strict'
module.exports = function definitionListPlugin (md) {
var isSpace = md.utils.isSpace
// Search `[:~][\n ]`, returns next pos after marker on success
// or -1 on fail.
function skipMarker (state, line) {
let start = state.bMarks[line] + state.tShift[line]
const max = state.eMarks[line]
if (start >= max) { return -1 }
// Check bullet
const marker = state.src.charCodeAt(start++)
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 }
const pos = state.skipSpaces(start)
// require space after ":"
if (start === pos) { return -1 }
return start
}
function markTightParagraphs (state, idx) {
const level = state.level + 2
let i
let l
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
state.tokens[i + 2].hidden = true
state.tokens[i].hidden = true
i += 2
}
}
}
function deflist (state, startLine, endLine, silent) {
var ch,
contentStart,
ddLine,
dtLine,
itemLines,
listLines,
listTokIdx,
max,
newEndLine,
nextLine,
offset,
oldDDIndent,
oldIndent,
oldLineMax,
oldParentType,
oldSCount,
oldTShift,
oldTight,
pos,
prevEmptyEnd,
tight,
token
if (silent) {
// quirk: validation mode validates a dd block only, not a whole deflist
if (state.ddIndent < 0) { return false }
return skipMarker(state, startLine) >= 0
}
nextLine = startLine + 1
if (nextLine >= endLine) { return false }
if (state.isEmpty(nextLine)) {
nextLine++
if (nextLine >= endLine) { return false }
}
if (state.sCount[nextLine] < state.blkIndent) { return false }
contentStart = skipMarker(state, nextLine)
if (contentStart < 0) { return false }
// Start list
listTokIdx = state.tokens.length
tight = true
token = state.push('dl_open', 'dl', 1)
token.map = listLines = [ startLine, 0 ]
//
// Iterate list items
//
dtLine = startLine
ddLine = nextLine
// One definition list can contain multiple DTs,
// and one DT can be followed by multiple DDs.
//
// Thus, there is two loops here, and label is
// needed to break out of the second one
//
/* eslint no-labels:0,block-scoped-var:0 */
OUTER:
for (;;) {
prevEmptyEnd = false
token = state.push('dt_open', 'dt', 1)
token.map = [ dtLine, dtLine ]
token = state.push('inline', '', 0)
token.map = [ dtLine, dtLine ]
token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim()
token.children = []
token = state.push('dt_close', 'dt', -1)
for (;;) {
token = state.push('dd_open', 'dd', 1)
token.map = itemLines = [ ddLine, 0 ]
pos = contentStart
max = state.eMarks[ddLine]
offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine])
while (pos < max) {
ch = state.src.charCodeAt(pos)
if (isSpace(ch)) {
if (ch === 0x09) {
offset += 4 - offset % 4
} else {
offset++
}
} else {
break
}
pos++
}
contentStart = pos
oldTight = state.tight
oldDDIndent = state.ddIndent
oldIndent = state.blkIndent
oldTShift = state.tShift[ddLine]
oldSCount = state.sCount[ddLine]
oldParentType = state.parentType
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
state.sCount[ddLine] = offset
state.tight = true
state.parentType = 'deflist'
newEndLine = ddLine
while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) {
}
oldLineMax = state.lineMax
state.lineMax = newEndLine
state.md.block.tokenize(state, ddLine, newEndLine, true)
state.lineMax = oldLineMax
// If any of list item is tight, mark list as tight
if (!state.tight || prevEmptyEnd) {
tight = false
}
// Item become loose if finish with empty line,
// but we should filter last element, because it means list finish
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1)
state.tShift[ddLine] = oldTShift
state.sCount[ddLine] = oldSCount
state.tight = oldTight
state.parentType = oldParentType
state.blkIndent = oldIndent
state.ddIndent = oldDDIndent
token = state.push('dd_close', 'dd', -1)
itemLines[1] = nextLine = state.line
if (nextLine >= endLine) { break OUTER }
if (state.sCount[nextLine] < state.blkIndent) { break OUTER }
contentStart = skipMarker(state, nextLine)
if (contentStart < 0) { break }
ddLine = nextLine
// go to the next loop iteration:
// insert DD tag and repeat checking
}
if (nextLine >= endLine) { break }
dtLine = nextLine
if (state.isEmpty(dtLine)) { break }
if (state.sCount[dtLine] < state.blkIndent) { break }
ddLine = dtLine + 1
if (ddLine >= endLine) { break }
if (state.isEmpty(ddLine)) { ddLine++ }
if (ddLine >= endLine) { break }
if (state.sCount[ddLine] < state.blkIndent) { break }
contentStart = skipMarker(state, ddLine)
if (contentStart < 0) { break }
// go to the next loop iteration:
// insert DT and DD tags and repeat checking
}
// Finilize list
token = state.push('dl_close', 'dl', -1)
listLines[1] = nextLine
state.line = nextLine
// mark paragraphs tight if needed
if (tight) {
markTightParagraphs(state, listTokIdx)
}
return true
}
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] })
}

View File

@@ -0,0 +1,136 @@
'use strict'
module.exports = function (md, renderers, defaultRenderer) {
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
function fence (state, startLine, endLine, silent) {
let pos = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine]
if (state.sCount[startLine] - state.blkIndent >= 4 || pos + 3 > max) {
return false
}
const marker = state.src.charCodeAt(pos)
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) {
return false
}
let mem = pos
pos = state.skipChars(pos, marker)
let len = pos - mem
if (len < 3) {
return false
}
const markup = state.src.slice(mem, pos)
const params = state.src.slice(pos, max)
if (silent) {
return true
}
let nextLine = startLine
let haveEndMarker = false
while (true) {
nextLine++
if (nextLine >= endLine) {
break
}
pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
max = state.eMarks[nextLine]
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
break
}
if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) {
continue
}
pos = state.skipChars(pos, marker)
if (pos - mem < len) {
continue
}
pos = state.skipSpaces(pos)
if (pos >= max) {
haveEndMarker = true
break
}
}
len = state.sCount[startLine]
state.line = nextLine + (haveEndMarker ? 1 : 0)
const parameters = {}
let langType = ''
let fileName = ''
let firstLineNumber = 1
let match = paramsRE.exec(params)
if (match) {
if (match[1]) {
langType = match[1]
}
if (match[3]) {
fileName = match[3]
}
if (match[4]) {
firstLineNumber = parseInt(match[4], 10)
}
if (match[2]) {
const params = match[2]
const regex = /(\w[-\w]*)(?:=(?:'(.*?[^\\])?'|"(.*?[^\\])?"|([^'"][^\s]*)))?/g
let name, value
while ((match = regex.exec(params))) {
name = match[1]
value = match[2] || match[3] || match[4] || null
const height = /^(\d+)h$/.exec(name)
if (height && !value) {
parameters.height = height[1]
} else {
parameters[name] = value
}
}
}
}
let token
if (renderers[langType]) {
token = state.push(`${langType}_fence`, 'div', 0)
} else {
token = state.push('_fence', 'code', 0)
}
token.langType = langType
token.fileName = fileName
token.firstLineNumber = firstLineNumber
token.parameters = parameters
token.content = state.getLines(startLine + 1, nextLine, len, true)
token.markup = markup
token.map = [startLine, state.line]
return true
}
md.block.ruler.before('fence', '_fence', fence, {
alt: ['paragraph', 'reference', 'blockquote', 'list']
})
for (const name in renderers) {
md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index])
}
if (defaultRenderer) {
md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index])
}
}

View File

@@ -2,6 +2,7 @@
import sanitizeHtml from 'sanitize-html'
import { escapeHtmlCharacters } from './utils'
import url from 'url'
module.exports = function sanitizePlugin (md, options) {
options = options || {}
@@ -14,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) {
options
)
}
if (state.tokens[tokenIdx].type === 'fence') {
if (state.tokens[tokenIdx].type === '_fence') {
// escapeHtmlCharacters has better performance
state.tokens[tokenIdx].content = escapeHtmlCharacters(
state.tokens[tokenIdx].content,
@@ -25,7 +26,7 @@ module.exports = function sanitizePlugin (md, options) {
const inlineTokens = state.tokens[tokenIdx].children
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
if (inlineTokens[childIdx].type === 'html_inline') {
inlineTokens[childIdx].content = sanitizeHtml(
inlineTokens[childIdx].content = sanitizeInline(
inlineTokens[childIdx].content,
options
)
@@ -35,3 +36,89 @@ module.exports = function sanitizePlugin (md, options) {
}
})
}
const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig
function sanitizeInline (html, options) {
let match = tagRegex.exec(html)
if (!match) {
return ''
}
const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options
if (match[1] !== undefined) {
// opening tag
const tag = match[1].toLowerCase()
if (allowedTags.indexOf(tag) === -1) {
return ''
}
const attributes = match[2]
let attrs = ''
let name
let value
while ((match = attributesRegex.exec(attributes))) {
name = match[1].toLowerCase()
value = match[3]
if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) {
continue
}
}
attrs += ` ${name}`
if (match[2]) {
attrs += `="${value}"`
}
}
}
if (selfClosing.indexOf(tag) === -1) {
return '<' + tag + attrs + '>'
} else {
return '<' + tag + attrs + ' />'
}
} else {
// closing tag
if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) {
return html
} else {
return ''
}
}
}
function naughtyHRef (href, options) {
// href = href.replace(/[\x00-\x20]+/g, '')
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
const matches = href.match(/^([a-zA-Z]+)\:/)
if (!matches) {
if (href.match(/^[\/\\]{2}/)) {
return !options.allowProtocolRelative
}
// No scheme
return false
}
const scheme = matches[1].toLowerCase()
return options.allowedSchemes.indexOf(scheme) === -1
}
function naughtyIFrame (src, options) {
try {
const parsed = url.parse(src, false, true)
return options.allowedIframeHostnames.index(parsed.hostname) === -1
} catch (e) {
return true
}
}

View File

@@ -5,6 +5,7 @@
import toc from 'markdown-toc'
import diacritics from 'diacritics-map'
import stripColor from 'strip-color'
import mdlink from 'markdown-link'
const EOL = require('os').EOL
@@ -42,6 +43,12 @@ function caseSensitiveSlugify (str) {
return str
}
function linkify (tok, text, slug, opts) {
var uniqeID = opts.num === 0 ? '' : '-' + opts.num
tok.content = mdlink(text, '#' + slug + uniqeID)
return tok
}
const TOC_MARKER_START = '<!-- toc -->'
const TOC_MARKER_END = '<!-- tocstop -->'
@@ -84,7 +91,7 @@ export function generateInEditor (editor) {
* @returns generatedTOC String containing generated TOC
*/
export function generate (markdownText) {
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify})
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify, linkify: linkify})
return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END
}

View File

@@ -7,6 +7,7 @@ import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
import { lastFindInArray } from './utils'
import anchor from '@enyaxu/markdown-it-anchor'
function createGutter (str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
@@ -27,32 +28,6 @@ class Markdown {
html: true,
xhtmlOut: true,
breaks: config.preview.breaks,
highlight: function (str, lang) {
const delimiter = ':'
const langInfo = lang.split(delimiter)
const langType = langInfo[0]
const fileName = langInfo[1] || ''
const firstLineNumber = parseInt(langInfo[2], 10)
if (langType === 'flowchart') {
return `<pre class="flowchart">${str}</pre>`
}
if (langType === 'sequence') {
return `<pre class="sequence">${str}</pre>`
}
if (langType === 'chart') {
return `<pre class="chart">${str}</pre>`
}
if (langType === 'mermaid') {
return `<pre class="mermaid">${str}</pre>`
}
return '<pre class="code CodeMirror">' +
'<span class="filename">' + fileName + '</span>' +
createGutter(str, firstLineNumber) +
'<code class="' + langType + '">' +
str +
'</code></pre>'
},
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,18 +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'))
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 `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="chart" data-height="${token.parameters.height}" data-format="${token.parameters.format || 'json'}">${token.content}</div>
</pre>`
},
flowchart: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
</pre>`
},
mermaid: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="mermaid" data-height="${token.parameters.height}">${token.content}</div>
</pre>`
},
sequence: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="sequence" data-height="${token.parameters.height}">${token.content}</div>
</pre>`
}
}, token => {
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
${createGutter(token.content, token.firstLineNumber)}
<code class="${token.langType}">${token.content}</code>
</pre>`
})
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) {
@@ -222,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 = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
}
@@ -247,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]])
}

View File

@@ -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
View 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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 <MarkdownEditor
ref='content'
@@ -321,7 +378,7 @@ class MarkdownNoteDetail extends React.Component {
}
render () {
const { data, location } = this.props
const { data, location, config } = this.props
const { note, editorType } = this.state
const storageKey = note.storage
const folderKey = note.folder
@@ -372,10 +429,12 @@ class MarkdownNoteDetail extends React.Component {
<TagSelect
ref='tags'
value={this.state.note.tags}
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
onChange={this.handleUpdateTag.bind(this)}
/>
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div>
<div styleName='info-right'>
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
@@ -429,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}

View File

@@ -76,3 +76,8 @@ body[data-theme="monokai"]
.root
border-left 1px solid $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
body[data-theme="dracula"]
.root
border-left 1px solid $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor

View File

@@ -98,8 +98,13 @@ body[data-theme="solarized-dark"]
.info
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor
body[data-theme="monokai"]
.info
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
background-color $ui-monokai-noteDetail-backgroundColor
body[data-theme="dracula"]
.info
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor

View File

@@ -112,7 +112,7 @@ class SnippetNoteDetail extends React.Component {
if (this.refs.tags) note.tags = this.refs.tags.value
note.description = this.refs.description.value
note.updatedAt = new Date()
note.title = findNoteTitle(note.description)
note.title = findNoteTitle(note.description, false)
this.setState({
note
@@ -354,12 +354,10 @@ class SnippetNoteDetail extends React.Component {
this.refs['code-' + this.state.snippetIndex].reload()
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
console.log('no need for arrows')
this.moveTabBarBy(0)
} else {
const lastTab = this.allTabs.lastChild
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
console.log('need to scroll')
const width = this.visibleTabs.offsetWidth
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
@@ -436,6 +434,18 @@ class SnippetNoteDetail extends React.Component {
this.focusEditor()
}
break
// I key
case 73:
{
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
if (isSuper) {
e.preventDefault()
this.handleInfoButtonClick(e)
}
}
break
// L key
case 76:
{
@@ -627,7 +637,6 @@ class SnippetNoteDetail extends React.Component {
}
focusEditor () {
console.log('code-' + this.state.snippetIndex)
this.refs['code-' + this.state.snippetIndex].focus()
}
@@ -759,6 +768,9 @@ class SnippetNoteDetail extends React.Component {
<TagSelect
ref='tags'
value={this.state.note.tags}
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
onChange={(e) => this.handleChange(e)}
/>
</div>

View File

@@ -169,4 +169,21 @@ body[data-theme="monokai"]
.tabList
background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color
color $ui-monokai-text-color
body[data-theme="dracula"]
.root
border-left 1px solid $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor
.body
background-color $ui-dracula-noteDetail-backgroundColor
.body .description textarea
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color
border 1px solid $ui-dracula-borderColor
.tabList
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color

View File

@@ -88,6 +88,11 @@ class TagSelect extends React.Component {
this.refs.newTag.input.focus()
}
handleTagLabelClick (tag) {
const { router } = this.context
router.push(`/tags/${tag}`)
}
handleTagRemoveButtonClick (tag) {
this.removeTagByCallback((value, tag) => {
value.splice(value.indexOf(tag), 1)
@@ -174,15 +179,15 @@ class TagSelect extends React.Component {
}
render () {
const { value, className } = this.props
const { value, className, showTagsAlphabetically } = this.props
const tagList = _.isArray(value)
? value.map((tag) => {
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
return (
<span styleName='tag'
key={tag}
>
<span styleName='tag-label'>#{tag}</span>
<span styleName='tag-label' onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
<button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
>
@@ -228,6 +233,10 @@ class TagSelect extends React.Component {
}
}
TagSelect.contextTypes = {
router: PropTypes.shape({})
}
TagSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),

View File

@@ -39,8 +39,9 @@
.tag-label
font-size 13px
color: $ui-text-color
color $ui-text-color
padding 4px 16px 4px 8px
cursor pointer
body[data-theme="dark"]
.tag
@@ -67,11 +68,22 @@ body[data-theme="solarized-dark"]
body[data-theme="monokai"]
.tag
background-color $ui-monokai-button-backgroundColor
background-color $ui-monokai-tag-backgroundColor
.tag-removeButton
border-color $ui-button--focus-borderColor
background-color transparent
.tag-label
color $ui-monokai-text-color
color $ui-monokai-text-color
body[data-theme="dracula"]
.tag
background-color $ui-dracula-tag-backgroundColor
.tag-removeButton
border-color $ui-dracula-button--focus-borderColor
background-color transparent
.tag-label
color $ui-dracula-borderColor

View File

@@ -1,26 +1,26 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleModeButton = ({
onClick, editorType
}) => (
<div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
</div>
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
</div>
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
</div>
)
ToggleModeButton.propTypes = {
onClick: PropTypes.func.isRequired,
editorType: PropTypes.string.Required
}
export default CSSModules(ToggleModeButton, styles)
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleModeButton = ({
onClick, editorType
}) => (
<div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
</div>
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
</div>
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
</div>
)
ToggleModeButton.propTypes = {
onClick: PropTypes.func.isRequired,
editorType: PropTypes.string.Required
}
export default CSSModules(ToggleModeButton, styles)

View File

@@ -59,7 +59,14 @@ body[data-theme="solarized-dark"]
body[data-theme="monokai"]
.control-toggleModeButton
background-color #272822
background-color #373831
.active
background-color #1EC38B
background-color #f92672
box-shadow 2px 0px 7px #222222
body[data-theme="dracula"]
.control-toggleModeButton
background-color #44475a
.active
background-color #bd93f9
box-shadow 2px 0px 7px #222222

View File

@@ -56,7 +56,7 @@ class Main extends React.Component {
init () {
dataApi
.addStorage({
name: 'My Storage',
name: 'My Storage Location',
path: path.join(remote.app.getPath('home'), 'Boostnote')
})
.then(data => {
@@ -80,7 +80,6 @@ class Main extends React.Component {
}
})
.then(data => {
console.log(data)
store.dispatch({
type: 'ADD_STORAGE',
storage: data.storage,
@@ -141,7 +140,7 @@ class Main extends React.Component {
componentDidMount () {
const { dispatch, config } = this.props
const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai']
const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai', 'dracula']
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
document.body.setAttribute('data-theme', config.ui.theme)
@@ -168,6 +167,8 @@ class Main extends React.Component {
}
})
delete CodeMirror.keyMap.emacs['Ctrl-V']
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
}
@@ -297,7 +298,7 @@ class Main extends React.Component {
onMouseUp={e => this.handleMouseUp(e)}
>
<SideNav
{..._.pick(this.props, ['dispatch', 'data', 'config', 'location'])}
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
width={this.state.navWidth}
/>
{!config.isSideNavFolded &&

View File

@@ -79,3 +79,7 @@ body[data-theme="solarized-dark"]
body[data-theme="monokai"]
.root, .root--expanded
background-color $ui-monokai-noteList-backgroundColor
body[data-theme="dracula"]
.root, .root--expanded
background-color $ui-dracula-noteList-backgroundColor

View File

@@ -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
})
}

View File

@@ -84,7 +84,7 @@ body[data-theme="dark"]
color $ui-dark-inactive-text-color
&:hover
color $ui-dark-text-color
.control-button--active
color $ui-dark-text-color
&:active
@@ -109,7 +109,7 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-inactive-text-color
&:hover
color $ui-solarized-dark-text-color
.control-button--active
color $ui-solarized-dark-text-color
&:active
@@ -138,3 +138,27 @@ body[data-theme="monokai"]
color $ui-monokai-text-color
&:active
color $ui-monokai-text-color
body[data-theme="dracula"]
.root
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
.control
background-color $ui-dracula-noteList-backgroundColor
border-color $ui-dracula-borderColor
.control-sortBy-select
&:hover
transition 0.2s
color $ui-dracula-text-color
.control-button
color $ui-dracula-inactive-text-color
&:hover
color $ui-dracula-text-color
.control-button--active
color $ui-dracula-text-color
&:active
color $ui-dracula-text-color

View File

@@ -56,7 +56,6 @@ class NoteList extends React.Component {
super(props)
this.selectNextNoteHandler = () => {
console.log('fired next')
this.selectNextNote()
}
this.selectPriorNoteHandler = () => {
@@ -84,7 +83,9 @@ class NoteList extends React.Component {
// TODO: not Selected noteKeys but SelectedNote(for reusing)
this.state = {
ctrlKeyDown: false,
shiftKeyDown: false,
prevShiftNoteIndex: -1,
selectedNoteKeys: []
}
@@ -267,7 +268,7 @@ class NoteList extends React.Component {
}
handleNoteListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true
if (e.metaKey) return true
// A key
if (e.keyCode === 65 && !e.shiftKey) {
@@ -307,6 +308,8 @@ class NoteList extends React.Component {
if (e.shiftKey) {
this.setState({ shiftKeyDown: true })
} else if (e.ctrlKey) {
this.setState({ ctrlKeyDown: true })
}
}
@@ -314,6 +317,10 @@ class NoteList extends React.Component {
if (!e.shiftKey) {
this.setState({ shiftKeyDown: false })
}
if (!e.ctrlKey) {
this.setState({ ctrlKeyDown: false })
}
}
getNotes () {
@@ -390,25 +397,65 @@ class NoteList extends React.Component {
return pinnedNotes.concat(unpinnedNotes)
}
getNoteIndexByKey (noteKey) {
return this.notes.findIndex((note) => {
if (!note) return -1
return note.key === noteKey
})
}
handleNoteClick (e, uniqueKey) {
const { router } = this.context
const { location } = this.props
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
const { ctrlKeyDown, shiftKeyDown } = this.state
const hasSelectedNoteKey = selectedNoteKeys.length > 0
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) {
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
this.setState({
selectedNoteKeys: newSelectedNoteKeys
})
return
}
if (!shiftKeyDown) {
if (!ctrlKeyDown && !shiftKeyDown) {
selectedNoteKeys = []
}
if (!shiftKeyDown) {
prevShiftNoteIndex = -1
}
selectedNoteKeys.push(uniqueKey)
if (shiftKeyDown && hasSelectedNoteKey) {
let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0])
// Shift selection can either start from first note in the exisiting selectedNoteKeys
// or previous first shift note index
firstShiftNoteIndex = firstShiftNoteIndex > prevShiftNoteIndex
? firstShiftNoteIndex : prevShiftNoteIndex
const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey)
const startIndex = firstShiftNoteIndex < lastShiftNoteIndex
? firstShiftNoteIndex : lastShiftNoteIndex
const endIndex = firstShiftNoteIndex > lastShiftNoteIndex
? firstShiftNoteIndex : lastShiftNoteIndex
selectedNoteKeys = []
for (let i = startIndex; i <= endIndex; i++) {
selectedNoteKeys.push(this.notes[i].key)
}
if (prevShiftNoteIndex < 0) {
prevShiftNoteIndex = firstShiftNoteIndex
}
}
this.setState({
selectedNoteKeys
selectedNoteKeys,
prevShiftNoteIndex
})
router.push({
@@ -519,7 +566,7 @@ class NoteList extends React.Component {
click: this.cloneNote.bind(this)
}, {
label: copyNoteLink,
click: this.copyNoteLink(note)
click: this.copyNoteLink.bind(this, note)
})
if (note.type === 'MARKDOWN_NOTE') {
if (note.blog && note.blog.blogLink && note.blog.blogId) {
@@ -616,7 +663,6 @@ class NoteList extends React.Component {
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
console.log('Notes were all deleted')
} else {
if (!confirmDeleteNote(confirmDeletion, false)) return
@@ -636,7 +682,6 @@ class NoteList extends React.Component {
})
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
console.log('Notes went to trash')
})
.catch((err) => {
console.error('Notes could not go to trash: ' + err)
@@ -996,6 +1041,7 @@ class NoteList extends React.Component {
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
/>
)
}

View File

@@ -19,7 +19,7 @@
text-align center
.top-menu-label
margin-left 5px
overflow ellipsis
@@ -122,3 +122,8 @@ body[data-theme="monokai"]
.root, .root--folded
background-color $ui-monokai-backgroundColor
border-right 1px solid $ui-monokai-borderColor
body[data-theme="dracula"]
.root, .root--folded
background-color $ui-dracula-backgroundColor
border-right 1px solid $ui-dracula-borderColor

View File

@@ -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)

View File

@@ -18,6 +18,12 @@ import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import { remote } from 'electron'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
function matchActiveTags (tags, activeTags) {
return _.every(activeTags, v => tags.indexOf(v) >= 0)
}
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
@@ -30,6 +36,52 @@ class SideNav extends React.Component {
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
}
deleteTag (tag) {
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
ype: 'warning',
message: i18n.__('Confirm tag deletion'),
detail: i18n.__('This will permanently remove this tag.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (selectedButton === 0) {
const { data, dispatch, location, params } = this.props
const notes = data.noteMap
.map(note => note)
.filter(note => note.tags.indexOf(tag) !== -1)
.map(note => {
note = Object.assign({}, note)
note.tags = note.tags.slice()
note.tags.splice(note.tags.indexOf(tag), 1)
return note
})
Promise
.all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
.then(updatedNotes => {
updatedNotes.forEach(note => {
dispatch({
type: 'UPDATE_NOTE',
note
})
})
if (location.pathname.match('/tags')) {
const tags = params.tagname.split(' ')
const index = tags.indexOf(tag)
if (index !== -1) {
tags.splice(index, 1)
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
}
}
})
}
}
handleMenuButtonClick (e) {
openModal(PreferencesModal)
}
@@ -44,6 +96,17 @@ class SideNav extends React.Component {
router.push('/starred')
}
handleTagContextMenu (e, tag) {
const menu = []
menu.push({
label: i18n.__('Delete Tag'),
click: this.deleteTag.bind(this, tag)
})
context.popup(menu)
}
handleToggleButtonClick (e) {
const { dispatch, config } = this.props
@@ -144,12 +207,20 @@ class SideNav extends React.Component {
tagListComponent () {
const { data, location, config } = this.props
const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
const activeTags = this.getActiveTags(location.pathname)
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
let tagList = _.sortBy(data.tagNoteMap.map(
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
), ['name']).filter(
).filter(
tag => tag.size > 0
)
), ['name'])
if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
const notesTags = data.noteMap.map(note => note.tags)
tagList = tagList.map(tag => {
tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length
return tag
})
}
if (config.sortTagsBy === 'COUNTER') {
tagList = _.sortBy(tagList, item => (0 - item.size))
}
@@ -165,6 +236,7 @@ class SideNav extends React.Component {
name={tag.name}
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
handleContextMenu={this.handleTagContextMenu.bind(this)}
isActive={this.getTagActive(location.pathname, tag.name)}
isRelated={tag.related}
key={tag.name}
@@ -198,7 +270,7 @@ class SideNav extends React.Component {
const tags = pathSegments[pathSegments.length - 1]
return (tags === 'alltags')
? []
: tags.split(' ').map(tag => decodeURIComponent(tag))
: decodeURIComponent(tags).split(' ')
}
handleClickTagListItem (name) {
@@ -230,7 +302,7 @@ class SideNav extends React.Component {
} else {
listOfTags.push(tag)
}
router.push(`/tags/${listOfTags.map(tag => encodeURIComponent(tag)).join(' ')}`)
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
}
emptyTrash (entries) {
@@ -238,6 +310,8 @@ class SideNav extends React.Component {
const deletionPromises = entries.map((note) => {
return dataApi.deleteNote(note.storage, note.key)
})
const { confirmDeletion } = this.props.config.ui
if (!confirmDeleteNote(confirmDeletion, true)) return
Promise.all(deletionPromises)
.then((arrayOfStorageAndNoteKeys) => {
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
@@ -247,7 +321,6 @@ class SideNav extends React.Component {
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
console.log('Trash emptied')
}
handleFilterButtonContextMenu (event) {

View File

@@ -34,7 +34,7 @@
color $ui-active-color
span
margin-left 5px
.update
navButtonColor()
height 24px
@@ -47,6 +47,14 @@
.update-icon
color $brand-color
body[data-theme="default"]
.zoom
color $ui-text-color
body[data-theme="white"]
.zoom
color $ui-text-color
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
@@ -80,3 +88,14 @@ body[data-theme="monokai"]
color $ui-monokai-active-color
&:active
color $ui-monokai-active-color
body[data-theme="dracula"]
navButtonColor()
.zoom
border-color $ui-dark-borderColor
color $ui-dracula-text-color
&:hover
transition 0.15s
color $ui-dracula-active-color
&:active
color $ui-dracula-active-color

View File

@@ -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

View File

@@ -256,3 +256,25 @@ body[data-theme="monokai"]
input
background-color $ui-monokai-noteList-backgroundColor
color $ui-monokai-text-color
body[data-theme="dracula"]
.root, .root--expanded
background-color $ui-dracula-noteList-backgroundColor
.control
border-color $ui-dracula-borderColor
.control-search
background-color $ui-dracula-noteList-backgroundColor
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color $ui-dracula-inactive-text-color
background-color $ui-dracula-noteList-backgroundColor
.control-search-input
background-color $ui-dracula-noteList-backgroundColor
input
background-color $ui-dracula-noteList-backgroundColor
color $ui-dracula-text-color

View File

@@ -18,6 +18,9 @@ body
::-webkit-scrollbar
width 12px
::-webkit-scrollbar-corner
background-color: transparent;
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.15)
@@ -162,6 +165,15 @@ body[data-theme="monokai"]
.sortableItemHelper
color: $ui-monokai-text-color
body[data-theme="dracula"]
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
.modalBack
background-color $ui-dracula-backgroundColor
.sortableItemHelper
color: $ui-dracula-text-color
body[data-theme="default"]
.SideNav ::-webkit-scrollbar-thumb
background-color rgba(255, 255, 255, 0.3)

View File

@@ -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)
}
}
}

View File

@@ -24,7 +24,8 @@ export const DEFAULT_CONFIG = {
amaEnabled: true,
hotkey: {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + M' : 'Ctrl + M'
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
},
ui: {
language: 'en',
@@ -43,11 +44,15 @@ export const DEFAULT_CONFIG = {
enableRulers: false,
rulers: [80, 120],
displayLineNumbers: true,
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
scrollPastEnd: false,
type: 'SPLIT',
type: 'SPLIT', // 'SPLIT', 'EDITOR_PREVIEW'
fetchUrlTitle: true,
enableTableEditor: false
enableTableEditor: false,
enableFrontMatterTitle: true,
frontMatterTitleField: 'title',
spellcheck: false
},
preview: {
fontSize: '14',
@@ -60,12 +65,14 @@ export const DEFAULT_CONFIG = {
latexBlockClose: '$$',
plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
scrollPastEnd: false,
scrollSync: true,
smartQuotes: true,
breaks: true,
smartArrows: false,
allowCustomCSS: false,
customCSS: '',
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
lineThroughCheckbox: true
},
blog: {
type: 'wordpress', // Available value: wordpress, add more types in the future plz
@@ -147,6 +154,8 @@ function set (updates) {
document.body.setAttribute('data-theme', 'solarized-dark')
} else if (newConfig.ui.theme === 'monokai') {
document.body.setAttribute('data-theme', 'monokai')
} else if (newConfig.ui.theme === 'dracula') {
document.body.setAttribute('data-theme', 'dracula')
} else {
document.body.setAttribute('data-theme', 'default')
}
@@ -195,6 +204,7 @@ function rewriteHotkey (config) {
const keys = [...Object.keys(config.hotkey)]
keys.forEach(key => {
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
})
return config
}

View File

@@ -11,6 +11,118 @@ import i18n from 'browser/lib/i18n'
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
const DESTINATION_FOLDER = 'attachments'
const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
/**
* @description
* Create a Image element to get the real size of image.
* @param {File} file the File object dropped.
* @returns {Promise<Image>} Image element created
*/
function getImage (file) {
return new Promise((resolve) => {
const reader = new FileReader()
const img = new Image()
img.onload = () => resolve(img)
reader.onload = e => {
img.src = e.target.result
}
reader.readAsDataURL(file)
})
}
/**
* @description
* Get the orientation info from iamges's EXIF data.
* case 1: The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
* case 2: The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
* case 3: The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
* case 4: The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
* case 5: The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
* case 6: The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
* case 7: The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
* case 8: The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
* Other: reserved
* ref: http://sylvana.net/jpegcrop/exif_orientation.html
* @param {File} file the File object dropped.
* @returns {Promise<Number>} Orientation info
*/
function getOrientation (file) {
const getData = arrayBuffer => {
const view = new DataView(arrayBuffer)
// Not start with SOI(Start of image) Marker return fail value
if (view.getUint16(0, false) !== 0xFFD8) return -2
const length = view.byteLength
let offset = 2
while (offset < length) {
const marker = view.getUint16(offset, false)
offset += 2
// Loop and seed for APP1 Marker
if (marker === 0xFFE1) {
// return fail value if it isn't EXIF data
if (view.getUint32(offset += 2, false) !== 0x45786966) {
return -1
}
// Read TIFF header,
// First 2bytes defines byte align of TIFF data.
// If it is 0x4949="II", it means "Intel" type byte align.
// If it is 0x4d4d="MM", it means "Motorola" type byte align
const little = view.getUint16(offset += 6, false) === 0x4949
offset += view.getUint32(offset + 4, little)
const tags = view.getUint16(offset, little) // Get TAG number
offset += 2
for (let i = 0; i < tags; i++) {
// Loop to find Orientation TAG and return the value
if (view.getUint16(offset + (i * 12), little) === 0x0112) {
return view.getUint16(offset + (i * 12) + 8, little)
}
}
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
break
} else {
offset += view.getUint16(offset, false)
}
}
return -1
}
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = event => resolve(getData(event.target.result))
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
})
}
/**
* @description
* Rotate image file to correct direction.
* Create a canvas and draw the image with correct direction, then export to base64 format.
* @param {*} file the File object dropped.
* @return {String} Base64 encoded image.
*/
function fixRotate (file) {
return Promise.all([getImage(file), getOrientation(file)])
.then(([img, orientation]) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (orientation > 4 && orientation < 9) {
canvas.width = img.height
canvas.height = img.width
} else {
canvas.width = img.width
canvas.height = img.height
}
switch (orientation) {
case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break
case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break
case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break
case 6: ctx.transform(0, 1, -1, 0, img.height, 0); break
case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break
case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break
default: break
}
ctx.drawImage(img, 0, 0)
return canvas.toDataURL()
})
}
/**
* @description
@@ -38,26 +150,34 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
}
try {
if (!fs.existsSync(sourceFilePath)) {
reject('source file does not exist')
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
if (!fs.existsSync(sourceFilePath) && !isBase64) {
return reject('source file does not exist')
}
const targetStorage = findStorage.findStorage(storageKey)
const inputFileStream = fs.createReadStream(sourceFilePath)
let destinationName
if (useRandomName) {
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}`
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath.sourceFilePath || sourceFilePath)}`
} else {
destinationName = path.basename(sourceFilePath)
destinationName = path.basename(sourceFilePath.sourceFilePath || sourceFilePath)
}
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey)
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
inputFileStream.pipe(outputFile)
inputFileStream.on('end', () => {
resolve(destinationName)
})
if (isBase64) {
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
const dataBuffer = new Buffer(base64Data, 'base64')
outputFile.write(dataBuffer, () => {
resolve(destinationName)
})
} else {
const inputFileStream = fs.createReadStream(sourceFilePath)
inputFileStream.pipe(outputFile)
inputFileStream.on('end', () => {
resolve(destinationName)
})
}
} catch (e) {
return reject(e)
}
@@ -137,10 +257,17 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
const filePath = file.path
const originalFileName = path.basename(filePath)
const fileType = file['type']
copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
const showPreview = fileType.startsWith('image')
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
const isImage = fileType.startsWith('image')
let promise
if (isImage) {
promise = fixRotate(file).then(base64data => {
return copyAttachment({type: 'base64', data: base64data, sourceFilePath: filePath}, storageKey, noteKey)
})
} else {
promise = copyAttachment(filePath, storageKey, noteKey)
}
promise.then((fileName) => {
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), isImage)
codeEditor.insertAttachmentMd(imageMd)
})
}
@@ -402,7 +529,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
return modifiedLinkText
})
} else {
console.log('One if the parameters was null -> Do nothing..')
return Promise.resolve(linkText)
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -14,7 +14,6 @@ function once (name, listener) {
}
function emit (name, ...args) {
console.log(name)
remote.getCurrentWindow().webContents.send(name, ...args)
}

View File

@@ -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
})
}
)

View File

@@ -3,5 +3,8 @@ import ee from 'browser/main/lib/eventEmitter'
module.exports = {
'toggleMode': () => {
ee.emit('topbar:togglemodebutton')
},
'deleteNote': () => {
ee.emit('hotkey:deletenote')
}
}

View File

@@ -128,3 +128,29 @@ body[data-theme="monokai"]
.control-confirmButton
colorMonokaiPrimaryButton()
body[data-theme="dracula"]
.root
modalDracula()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-dracula-text-color
.control-folder-label
color $ui-dracula-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorDraculaPrimaryButton()

View File

@@ -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)
})
}

View File

@@ -97,3 +97,20 @@ body[data-theme="monokai"]
.description
color $ui-monokai-text-color
body[data-theme="dracula"]
.root
background-color transparent
.header
color $ui-dracula-text-color
.control-button
border-color $ui-dracula-borderColor
color $ui-dracula-text-color
background-color transparent
&:focus
colorDraculaPrimaryButton()
.description
color $ui-dracula-text-color

View File

@@ -43,7 +43,7 @@ class Blog extends React.Component {
this.handleSettingError = (err) => {
this.setState({BlogAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('Error occurs!')
message: err.message != null ? err.message : i18n.__('An error occurred!')
}})
}
this.oldBlog = this.state.config.blog
@@ -70,7 +70,7 @@ class Blog extends React.Component {
this.props.haveToSave({
tab: 'Blog',
type: 'warning',
message: i18n.__('You have to save!')
message: i18n.__('Unsaved Changes!')
})
}
}

View File

@@ -68,9 +68,9 @@
:global
.alert
display inline-block
position absolute
top 60px
right 15px
position fixed
top 130px
right 100px
font-size 14px
.success
color #1EC38B
@@ -127,7 +127,7 @@ colorDarkControl()
border-color $ui-dark-borderColor
background-color $ui-dark-backgroundColor
color $ui-dark-text-color
colorSolarizedDarkControl()
border none
background-color $ui-solarized-dark-button-backgroundColor
@@ -138,6 +138,10 @@ colorMonokaiControl()
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
colorDraculaControl()
border none
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
body[data-theme="dark"]
.root
@@ -165,27 +169,27 @@ body[data-theme="dark"]
.group-section-control
select, .group-section-control-input
colorDarkControl()
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
.group-header
color $ui-solarized-dark-text-color
border-color $ui-solarized-dark-borderColor
border-color $ui-solarized-dark-borderColor
.group-header2
color $ui-solarized-dark-text-color
.group-section-control-input
border-color $ui-solarized-dark-borderColor
border-color $ui-solarized-dark-borderColor
.group-control
border-color $ui-solarized-dark-borderColor
border-color $ui-solarized-dark-borderColor
.group-control-leftButton
colorDarkDefaultButton()
border-color $ui-solarized-dark-borderColor
border-color $ui-solarized-dark-borderColor
.group-control-rightButton
colorSolarizedDarkPrimaryButton()
.group-hint
@@ -200,19 +204,19 @@ body[data-theme="monokai"]
.group-header
color $ui-monokai-text-color
border-color $ui-monokai-borderColor
border-color $ui-monokai-borderColor
.group-header2
color $ui-monokai-text-color
.group-section-control-input
border-color $ui-monokai-borderColor
border-color $ui-monokai-borderColor
.group-control
border-color $ui-monokai-borderColor
border-color $ui-monokai-borderColor
.group-control-leftButton
colorDarkDefaultButton()
border-color $ui-monokai-borderColor
border-color $ui-monokai-borderColor
.group-control-rightButton
colorMonokaiPrimaryButton()
.group-hint
@@ -220,3 +224,30 @@ body[data-theme="monokai"]
.group-section-control
select, .group-section-control-input
colorMonokaiControl()
body[data-theme="dracula"]
.root
color $ui-dracula-text-color
.group-header
color $ui-dracula-text-color
border-color $ui-dracula-borderColor
.group-header2
color $ui-dracula-text-color
.group-section-control-input
border-color $ui-dracula-borderColor
.group-control
border-color $ui-dracula-borderColor
.group-control-leftButton
colorDarkDefaultButton()
border-color $ui-dracula-borderColor
.group-control-rightButton
colorDraculaPrimaryButton()
.group-hint
colorDraculaControl()
.group-section-control
select, .group-section-control-input
colorDraculaControl()

View File

@@ -23,21 +23,29 @@ class Crowdfunding extends React.Component {
return (
<div styleName='root'>
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
<p>{i18n.__('Dear everyone,')}</p>
<br />
<p>{i18n.__('Thank you for using Boostnote!')}</p>
<p>{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}</p>
<br />
<p>{i18n.__('To continue supporting this growth, and to satisfy community expectations,')}</p>
<p>{i18n.__('we would like to invest more time and resources in this project.')}</p>
<p>{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}</p>
<p>{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.')}</p>
<br />
<p>{i18n.__('If you like this project and see its potential, you can help by supporting us on OpenCollective!')}</p>
<p>{i18n.__('### Sustainable Open Source Ecosystem')}</p>
<p>{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.')}</p>
<p>{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. Weve got tons of Github stars and hundred of contributors in two years.')}</p>
<p>{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}</p>
<br />
<p>{i18n.__('Thanks,')}</p>
<p>{i18n.__('Boostnote maintainers')}</p>
<p>{i18n.__('### We believe Meritocracy')}</p>
<p>{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}</p>
<p>{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.')}</p>
<p>{i18n.__('It sometimes looks like exploitation.')}</p>
<p>{i18n.__('Weve realized IssueHunt could enhance sustainability of open-source ecosystem.')}</p>
<br />
<p>{i18n.__('As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.')}</p>
<br />
<p>{i18n.__('Thank you,')}</p>
<p>{i18n.__('The Boostnote Team')}</p>
<br />
<button styleName='cf-link'>
<a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('Support via OpenCollective')}</a>
<a href='http://bit.ly/issuehunt-from-boostnote-app' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('See IssueHunt')}</a>
</button>
</div>
)

View File

@@ -29,7 +29,7 @@ p
body[data-theme="dark"]
p
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
@@ -41,3 +41,9 @@ body[data-theme="monokai"]
color $ui-monokai-text-color
p
color $ui-monokai-text-color
body[data-theme="dracula"]
.root
color $ui-dracula-text-color
p
color $ui-dracula-text-color

View File

@@ -2,6 +2,7 @@
height 35px
box-sizing border-box
padding 2.5px 15px
display flex
&:hover
background-color darken(white, 3%)
@@ -18,7 +19,10 @@
border-left solid 2px transparent
padding 0 10px
line-height 30px
float left
flex 1
white-space nowrap
text-overflow ellipsis
overflow hidden
.folderItem-left-danger
color $danger-color
font-weight bold
@@ -52,7 +56,8 @@
outline none
.folderItem-right
float right
-webkit-box-flex: 1
white-space nowrap
.folderItem-right-button
vertical-align middle
@@ -149,3 +154,26 @@ body[data-theme="monokai"]
.folderItem-right-dangerButton
colorMonokaiPrimaryButton()
body[data-theme="dracula"]
.folderItem
&:hover
background-color $ui-dracula-button-backgroundColor
.folderItem-left-danger
color $danger-color
.folderItem-left-key
color $ui-dark-inactive-text-color
.folderItem-left-colorButton
colorDraculaPrimaryButton()
.folderItem-right-button
colorDraculaPrimaryButton()
.folderItem-right-confirmButton
colorDraculaPrimaryButton()
.folderItem-right-dangerButton
colorDraculaPrimaryButton()

View File

@@ -28,10 +28,20 @@ class HotkeyTab extends React.Component {
}})
}
this.handleSettingError = (err) => {
this.setState({keymapAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('Error occurs!')
}})
if (
this.state.config.hotkey.toggleMain === '' ||
this.state.config.hotkey.toggleMode === ''
) {
this.setState({keymapAlert: {
type: 'success',
message: i18n.__('Successfully applied!')
}})
} else {
this.setState({keymapAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('An error occurred!')
}})
}
}
this.oldHotkey = this.state.config.hotkey
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
@@ -68,7 +78,8 @@ class HotkeyTab extends React.Component {
const { config } = this.state
config.hotkey = {
toggleMain: this.refs.toggleMain.value,
toggleMode: this.refs.toggleMode.value
toggleMode: this.refs.toggleMode.value,
deleteNote: this.refs.deleteNote.value
}
this.setState({
config
@@ -79,7 +90,7 @@ class HotkeyTab extends React.Component {
this.props.haveToSave({
tab: 'Hotkey',
type: 'warning',
message: i18n.__('You have to save!')
message: i18n.__('Unsaved Changes!')
})
}
}
@@ -117,7 +128,7 @@ class HotkeyTab extends React.Component {
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Toggle editor mode')}</div>
<div styleName='group-section-label'>{i18n.__('Toggle Editor Mode')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
@@ -127,6 +138,17 @@ class HotkeyTab extends React.Component {
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Delete Note')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='deleteNote'
value={config.hotkey.deleteNote}
type='text'
/>
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-leftButton'
onClick={(e) => this.handleHintToggleButtonClick(e)}

View File

@@ -84,7 +84,7 @@ class InfoTab extends React.Component {
>{i18n.__('GitHub')}</a>
</li>
<li>
<a href='https://boostlog.io/@junp1234'
<a href='https://medium.com/boostnote'
onClick={(e) => this.handleLinkClick(e)}
>{i18n.__('Blog')}</a>
</li>

View File

@@ -60,7 +60,7 @@
body[data-theme="dark"]
.root
color alpha($tab--dark-text-color, 80%)
body[data-theme="solarized-dark"]
.root
@@ -75,3 +75,10 @@ body[data-theme="monokai"]
.list
a
color $ui-monokai-active-color
body[data-theme="dracula"]
.root
color $ui-dracula-text-color
.list
a
color $ui-dracula-active-color

View File

@@ -90,7 +90,7 @@ body[data-theme="dark"]
background-color $dark-primary-button-background--active
&:hover
color white
body[data-theme="solarized-dark"]
.root
@@ -139,3 +139,27 @@ body[data-theme="monokai"]
background-color $ui-monokai-button--active-backgroundColor
&:hover
color white
body[data-theme="dracula"]
.root
background-color transparent
.top-bar
background-color transparent
border-color $ui-dracula-borderColor
p
color $ui-dracula-text-color
.nav
background-color transparent
border-color $ui-dracula-borderColor
.nav-button
background-color transparent
color $ui-dracula-text-color
&:hover
color $ui-dracula-text-color
.nav-button--active
@extend .nav-button
color $ui-dracula-button--active-color
background-color $ui-dracula-button--active-backgroundColor
&:hover
color #f8f8f2

View File

@@ -55,7 +55,11 @@ class SnippetList extends React.Component {
defineSnippetStyleName (snippet) {
const { currentSnippet } = this.props
if (currentSnippet == null) return
if (currentSnippet == null) {
return 'snippet-item'
}
if (currentSnippet.id === snippet.id) {
return 'snippet-item-selected'
} else {

View File

@@ -6,6 +6,9 @@ import i18n from 'browser/lib/i18n'
import dataApi from 'browser/main/lib/dataApi'
import SnippetList from './SnippetList'
import eventEmitter from 'browser/main/lib/eventEmitter'
import copy from 'copy-to-clipboard'
const path = require('path')
class SnippetTab extends React.Component {
constructor (props) {
@@ -16,6 +19,17 @@ class SnippetTab extends React.Component {
this.changeDelay = null
}
notify (title, options) {
if (global.process.platform === 'win32') {
options.icon = path.join(
'file://',
global.__dirname,
'../../resources/app.png'
)
}
return new window.Notification(title, options)
}
handleSnippetNameOrPrefixChange () {
clearTimeout(this.changeDelay)
this.changeDelay = setTimeout(() => {
@@ -27,12 +41,14 @@ class SnippetTab extends React.Component {
handleSnippetSelect (snippet) {
const { currentSnippet } = this.state
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
// notify the snippet editor to load the content of the new snippet
this.snippetEditor.onSnippetChanged(changedSnippet)
this.setState({currentSnippet: changedSnippet})
})
if (snippet !== null) {
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
// notify the snippet editor to load the content of the new snippet
this.snippetEditor.onSnippetChanged(changedSnippet)
this.setState({currentSnippet: changedSnippet})
})
}
}
}
@@ -54,6 +70,17 @@ class SnippetTab extends React.Component {
}
}
handleCopySnippet (e) {
const showCopyNotification = this.props.config.ui.showCopyNotification
copy(this.state.currentSnippet.content)
if (showCopyNotification) {
this.notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
}
}
render () {
const { config, storageKey } = this.props
const { currentSnippet } = this.state
@@ -70,6 +97,13 @@ class SnippetTab extends React.Component {
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
currentSnippet={currentSnippet} />
<div styleName='snippet-detail' style={{visibility: currentSnippet ? 'visible' : 'hidden'}}>
<div styleName='group-section'>
<div styleName='group-section-control'>
<button styleName='group-control-rightButton'
onClick={e => this.handleCopySnippet(e)}>{i18n.__('Copy')}
</button>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Snippet name')}</div>
<div styleName='group-section-control'>

View File

@@ -196,3 +196,19 @@ body[data-theme="monokai"]
color white
.group-control-button
colorMonokaiPrimaryButton()
body[data-theme="dracula"]
.snippets
background $ui-dracula-backgroundColor
.snippet-item
color #f8f8f2
&::after
background $ui-dracula-borderColor
&:hover
background darken($ui-dracula-backgroundColor, 5)
.snippet-item-selected
background darken($ui-dracula-backgroundColor, 5)
.snippet-detail
color #f8f8f2
.group-control-button
colorDraculaPrimaryButton()

View File

@@ -9,13 +9,17 @@
box-sizing border-box
border-bottom $default-border
margin-bottom 5px
display flex
.header-label
float left
cursor pointer
&:hover
.header-label-editButton
opacity 1
flex 1
white-space nowrap
text-overflow ellipsis
overflow hidden
.header-label-path
color $ui-inactive-text-color
@@ -38,8 +42,8 @@
outline none
.header-control
float right
-webkit-box-flex: 1
white-space nowrap
.header-control-button
width 30px
height 25px

View File

@@ -70,7 +70,7 @@ class StoragesTab extends React.Component {
})
return (
<div styleName='list'>
<div styleName='header'>{i18n.__('Storages')}</div>
<div styleName='header'>{i18n.__('Storage Locations')}</div>
{storageList.length > 0
? storageList
: <div styleName='list-empty'>{i18n.__('No storage found.')}</div>

View File

@@ -158,7 +158,7 @@ body[data-theme="dark"]
.addStorage-body-control-cancelButton
colorDarkDefaultButton()
border-color $ui-dark-borderColor
body[data-theme="solarized-dark"]
@@ -236,3 +236,41 @@ body[data-theme="monokai"]
.addStorage-body-control-cancelButton
colorDarkDefaultButton()
border-color $ui-monokai-borderColor
body[data-theme="dracula"]
.root
color $ui-dracula-text-color
.folderList-item
border-bottom $ui-dracula-borderColor
.folderList-empty
color $ui-dracula-text-color
.list-empty
color $ui-dracula-text-color
.list-control-addStorageButton
border-color $ui-dracula-button-backgroundColor
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.addStorage-header
color $ui-dracula-text-color
border-color $ui-dracula-borderColor
.addStorage-body-section-name-input
border-color $$ui-dracula-borderColor
.addStorage-body-section-type-description
color $ui-dracula-text-color
.addStorage-body-section-path-button
colorPrimaryButton()
.addStorage-body-control
border-color $ui-dracula-borderColor
.addStorage-body-control-createButton
colorDarkPrimaryButton()
.addStorage-body-control-cancelButton
colorDarkDefaultButton()
border-color $ui-dracula-borderColor

View File

@@ -40,7 +40,7 @@ class UiTab extends React.Component {
this.handleSettingError = (err) => {
this.setState({UiAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('Error occurs!')
message: err.message != null ? err.message : i18n.__('An error occurred!')
}})
}
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
@@ -68,9 +68,13 @@ class UiTab extends React.Component {
theme: this.refs.uiTheme.value,
language: this.refs.uiLanguage.value,
defaultNote: this.refs.defaultNote.value,
tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags.checked,
showCopyNotification: this.refs.showCopyNotification.checked,
confirmDeletion: this.refs.confirmDeletion.checked,
showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
showTagsAlphabetically: this.refs.showTagsAlphabetically.checked,
saveTagsAlphabetically: this.refs.saveTagsAlphabetically.checked,
enableLiveNoteCounts: this.refs.enableLiveNoteCounts.checked,
disableDirectWrite: this.refs.uiD2w != null
? this.refs.uiD2w.checked
: false
@@ -89,7 +93,10 @@ class UiTab extends React.Component {
snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value,
scrollPastEnd: this.refs.scrollPastEnd.checked,
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked,
enableTableEditor: this.refs.enableTableEditor.checked
enableTableEditor: this.refs.enableTableEditor.checked,
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
frontMatterTitleField: this.refs.frontMatterTitleField.value,
spellcheck: this.refs.spellcheck.checked
},
preview: {
fontSize: this.refs.previewFontSize.value,
@@ -102,11 +109,13 @@ class UiTab extends React.Component {
latexBlockClose: this.refs.previewLatexBlockClose.value,
plantUMLServerAddress: this.refs.previewPlantUMLServerAddress.value,
scrollPastEnd: this.refs.previewScrollPastEnd.checked,
scrollSync: this.refs.previewScrollSync.checked,
smartQuotes: this.refs.previewSmartQuotes.checked,
breaks: this.refs.previewBreaks.checked,
smartArrows: this.refs.previewSmartArrows.checked,
sanitize: this.refs.previewSanitize.value,
allowCustomCSS: this.refs.previewAllowCustomCSS.checked,
lineThroughCheckbox: this.refs.lineThroughCheckbox.checked,
customCSS: this.customCSSCM.getCodeMirror().getValue()
}
}
@@ -125,7 +134,7 @@ class UiTab extends React.Component {
this.props.haveToSave({
tab: 'UI',
type: 'warning',
message: i18n.__('You have to save!')
message: i18n.__('Unsaved Changes!')
})
}
})
@@ -187,6 +196,7 @@ class UiTab extends React.Component {
<option value='white'>{i18n.__('White')}</option>
<option value='solarized-dark'>{i18n.__('Solarized Dark')}</option>
<option value='monokai'>{i18n.__('Monokai')}</option>
<option value='dracula'>{i18n.__('Dracula')}</option>
<option value='dark'>{i18n.__('Dark')}</option>
</select>
</div>
@@ -244,16 +254,6 @@ class UiTab extends React.Component {
{i18n.__('Show a confirmation dialog when deleting notes')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.showOnlyRelatedTags}
ref='showOnlyRelatedTags'
type='checkbox'
/>&nbsp;
{i18n.__('Show only related tags')}
</label>
</div>
{
global.process.platform === 'win32'
? <div styleName='group-checkBoxSection'>
@@ -269,6 +269,64 @@ class UiTab extends React.Component {
</div>
: null
}
<div styleName='group-header2'>Tags</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.saveTagsAlphabetically}
ref='saveTagsAlphabetically'
type='checkbox'
/>&nbsp;
{i18n.__('Save tags of a note in alphabetical order')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.showTagsAlphabetically}
ref='showTagsAlphabetically'
type='checkbox'
/>&nbsp;
{i18n.__('Show tags of a note in alphabetical order')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.showOnlyRelatedTags}
ref='showOnlyRelatedTags'
type='checkbox'
/>&nbsp;
{i18n.__('Show only related tags')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.enableLiveNoteCounts}
ref='enableLiveNoteCounts'
type='checkbox'
/>&nbsp;
{i18n.__('Enable live count of notes')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.tagNewNoteWithFilteringTags}
ref='tagNewNoteWithFilteringTags'
type='checkbox'
/>&nbsp;
{i18n.__('New notes are tagged with the filtering tags')}
</label>
</div>
<div styleName='group-header2'>Editor</div>
<div styleName='group-section'>
@@ -426,6 +484,31 @@ class UiTab extends React.Component {
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Front matter title field')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='frontMatterTitleField'
value={config.editor.frontMatterTitleField}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.enableFrontMatterTitle}
ref='enableFrontMatterTitle'
type='checkbox'
/>&nbsp;
{i18n.__('Extract title from front matter')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
@@ -469,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'
/>&nbsp;
{i18n.__('Enable spellcheck - Experimental feature!! :)')}
</label>
</div>
<div styleName='group-header2'>{i18n.__('Preview')}</div>
<div styleName='group-section'>
@@ -498,7 +591,7 @@ class UiTab extends React.Component {
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Code block Theme')}</div>
<div styleName='group-section-label'>{i18n.__('Code Block Theme')}</div>
<div styleName='group-section-control'>
<select value={config.preview.codeBlockTheme}
ref='previewCodeBlockTheme'
@@ -512,6 +605,16 @@ class UiTab extends React.Component {
</select>
</div>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.lineThroughCheckbox}
ref='lineThroughCheckbox'
type='checkbox'
/>&nbsp;
{i18n.__('Allow line through checkbox')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
@@ -522,6 +625,16 @@ class UiTab extends React.Component {
{i18n.__('Allow preview to scroll past the last line')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.scrollSync}
ref='previewScrollSync'
type='checkbox'
/>&nbsp;
{i18n.__('When scrolling, synchronize preview with editor')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}

View File

@@ -113,7 +113,6 @@ function data (state = defaultDataMap(), action) {
// If storage chanced, origin key must be discarded
if (originKey !== uniqueKey) {
console.log('diffrent storage')
// From isStarred
if (originNote.isStarred) {
state.starredSet = new Set(state.starredSet)

View File

@@ -14,18 +14,18 @@
list-style none
padding 0
margin 0
border-radius 4px
margin .2em 0 0
margin .5rem 0 0
background-color $ui-noteList-backgroundColor
border 1px solid rgba(0,0,0,.3)
box-shadow .05em .2em .6em rgba(0,0,0,.2)
text-shadow none
&:empty,
&[hidden]
display none
&:before
content ""
position absolute
@@ -39,12 +39,12 @@
border-bottom 0
-webkit-transform rotate(45deg)
transform rotate(45deg)
li
position relative
padding 6px 18px 6px 10px
cursor pointer
li[aria-selected="true"]
background-color alpha($ui-button--active-backgroundColor, 40%)
color $ui-text-color
@@ -53,15 +53,15 @@ body[data-theme="dark"]
.TagSelect
.react-autosuggest__input
color $ui-dark-text-color
ul
border-color $ui-dark-borderColor
background-color $ui-dark-noteList-backgroundColor
color $ui-dark-text-color
&:before
background-color $ui-dark-noteList-backgroundColor
li[aria-selected="true"]
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-text-color
@@ -70,32 +70,49 @@ body[data-theme="monokai"]
.TagSelect
.react-autosuggest__input
color $ui-monokai-text-color
ul
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteList-backgroundColor
color $ui-monokai-text-color
&:before
background-color $ui-dark-noteList-backgroundColor
li[aria-selected="true"]
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
body[data-theme="dracula"]
.TagSelect
.react-autosuggest__input
color $ui-dracula-text-color
ul
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
color $ui-dracula-text-color
&:before
background-color $ui-dark-noteList-backgroundColor
li[aria-selected="true"]
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
body[data-theme="solarized-dark"]
.TagSelect
.react-autosuggest__input
color $ui-solarized-dark-text-color
ul
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteList-backgroundColor
color $ui-solarized-dark-text-color
&:before
background-color $ui-solarized-dark-noteList-backgroundColor
li[aria-selected="true"]
background-color $ui-dark-button--active-backgroundColor
color $ui-solarized-dark-text-color
@@ -104,6 +121,6 @@ body[data-theme="white"]
.TagSelect
ul
background-color $ui-white-noteList-backgroundColor
li[aria-selected="true"]
background-color $ui-button--active-backgroundColor

View File

@@ -128,6 +128,16 @@ colorMonokaiPrimaryButton()
&:active:hover
background-color $dark-primary-button-background--active
colorDraculaPrimaryButton()
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
border none
&:hover
background-color $ui-dracula-button--active-backgroundColor
&:active
&:active:hover
background-color $ui-dracula-button--active-backgroundColor
// Danger button(Brand color)
$danger-button-background = #c9302c
@@ -369,7 +379,7 @@ $ui-monokai-active-color = #f92672
$ui-monokai-borderColor = #373831
$ui-monokai-tag-backgroundColor = #f92672
$ui-monokai-tag-backgroundColor = #373831
$ui-monokai-button-backgroundColor = #373831
$ui-monokai-button--active-color = white
@@ -383,4 +393,30 @@ modalMonokai()
width 100%
background-color $ui-monokai-backgroundColor
overflow hidden
border-radius $modal-border-radius
/******* Dracula theme ********/
$ui-dracula-backgroundColor = #282a36
$ui-dracula-noteList-backgroundColor = #282a36
$ui-dracula-noteDetail-backgroundColor = #282a36
$ui-dracula-text-color = #f8f8f2
$ui-dracula-active-color = #bd93f9
$ui-dracula-borderColor = #44475a
$ui-dracula-tag-backgroundColor = #8be9fd
$ui-dracula-button-backgroundColor = #44475a
$ui-dracula-button--active-color = #f8f8f2
$ui-dracula-button--active-backgroundColor = #bd93f9
$ui-dracula-button--hover-backgroundColor = lighten($ui-dracula-backgroundColor, 10%)
$ui-dracula-button--focus-borderColor = lighten(#44475a, 25%)
modalDracula()
position relative
z-index $modal-z-index
width 100%
background-color $ui-dracula-backgroundColor
overflow hidden
border-radius $modal-border-radius