diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index c36a50c1..41d71622 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -14,6 +14,8 @@ import consts from 'browser/lib/consts'
import fs from 'fs'
const { ipcRenderer } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
+import TurndownService from 'turndown'
+import { gfm } from 'turndown-plugin-gfm'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
@@ -57,6 +59,7 @@ 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.editorActivityHandler = () => this.handleEditorActivity()
@@ -125,6 +128,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 = [
{
@@ -475,7 +479,13 @@ export default class CodeEditor extends React.Component {
moveCursorTo (row, col) {}
- scrollToLine (num) {}
+ scrollToLine (event, num) {
+ const cursor = {
+ line: num,
+ ch: 1
+ }
+ this.editor.setCursor(cursor)
+ }
focus () {
this.editor.focus()
@@ -538,7 +548,11 @@ export default class CodeEditor extends React.Component {
)
return prevChar === '](' && nextChar === ')'
}
- if (dataTransferItem.type.match('image')) {
+
+ const pastedHtml = clipboardData.getData('text/html')
+ if (pastedHtml !== '') {
+ this.handlePasteHtml(e, editor, pastedHtml)
+ } else if (dataTransferItem.type.match('image')) {
attachmentManagement.handlePastImageEvent(
this,
storageKey,
@@ -608,6 +622,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) => {
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index 70df16a0..9c8a06d6 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -6,6 +6,7 @@ import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { findStorage } from 'browser/lib/findStorage'
+import ConfigManager from 'browser/main/lib/ConfigManager'
class MarkdownEditor extends React.Component {
constructor (props) {
@@ -18,7 +19,7 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186]
this.state = {
- status: 'PREVIEW',
+ status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
renderValue: props.value,
keyPressed: new Set(),
isLocked: false
@@ -64,6 +65,10 @@ class MarkdownEditor extends React.Component {
})
}
+ setValue (value) {
+ this.refs.code.setValue(value)
+ }
+
handleChange (e) {
this.value = this.refs.code.value
this.props.onChange(e)
@@ -72,9 +77,7 @@ class MarkdownEditor extends React.Component {
handleContextMenu (e) {
const { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') {
- const newStatus = this.state.status === 'PREVIEW'
- ? 'CODE'
- : 'PREVIEW'
+ const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
this.setState({
status: newStatus
}, () => {
@@ -84,6 +87,10 @@ class MarkdownEditor extends React.Component {
this.refs.preview.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
+
+ const newConfig = Object.assign({}, config)
+ newConfig.editor.delfaultStatus = newStatus
+ ConfigManager.set(newConfig)
})
}
}
@@ -300,6 +307,7 @@ class MarkdownEditor extends React.Component {
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
+ lineThroughCheckbox={config.preview.lineThroughCheckbox}
/>
)
diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js
index a5ec89b5..d9ff7074 100755
--- a/browser/components/MarkdownPreview.js
+++ b/browser/components/MarkdownPreview.js
@@ -81,7 +81,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 {
@@ -89,6 +88,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);
@@ -145,6 +149,8 @@ body p {
display: none
}
}
+
+${allowCustomCSS ? customCSS : ''}
`
}
@@ -326,9 +332,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,
@@ -485,10 +489,6 @@ export default class MarkdownPreview extends React.Component {
eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
eventEmitter.on('print', this.printHandler)
- eventEmitter.on('config-renew', () => {
- this.markdown.updateConfig()
- this.rewriteIframe()
- })
}
componentWillUnmount () {
@@ -532,7 +532,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()
@@ -866,6 +867,15 @@ export default class MarkdownPreview extends React.Component {
return
}
+ const regexIsLine = /^:line:[0-9]/
+ if (regexIsLine.test(linkHash)) {
+ const numberPattern = /\d+/g
+
+ const lineNumber = parseInt(linkHash.match(numberPattern)[0])
+ eventEmitter.emit('line:jump', lineNumber)
+ return
+ }
+
// this will match the old link format storage.key-note.key
// e.g.
// 877f99c3268608328037-1c211eb7dcb463de6490
diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js
index d714125a..ca2d3108 100644
--- a/browser/components/MarkdownSplitEditor.js
+++ b/browser/components/MarkdownSplitEditor.js
@@ -20,12 +20,18 @@ class MarkdownSplitEditor extends React.Component {
}
}
+ setValue (value) {
+ this.refs.code.setValue(value)
+ }
+
handleOnChange () {
this.value = this.refs.code.value
this.props.onChange()
}
handleScroll (e) {
+ if (!this.props.config.preview.scrollSync) return
+
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
const codeDoc = _.get(this, 'refs.code.editor.doc')
let srcTop, srcHeight, targetTop, targetHeight
@@ -192,6 +198,7 @@ class MarkdownSplitEditor extends React.Component {
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
+ lineThroughCheckbox={config.preview.lineThroughCheckbox}
/>
)
diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js
index 600b7e2d..2fc70a39 100644
--- a/browser/components/NoteItem.js
+++ b/browser/components/NoteItem.js
@@ -24,16 +24,19 @@ const TagElement = ({ tagName }) => (
/**
* @description Tag element list component.
* @param {Array|null} tags
+ * @param {boolean} showTagsAlphabetically
* @return {React.Component}
*/
-const TagElementList = tags => {
+const TagElementList = (tags, showTagsAlphabetically) => {
if (!isArray(tags)) {
return []
}
- const tagElements = tags.map(tag => TagElement({ tagName: tag }))
-
- return tagElements
+ if (showTagsAlphabetically) {
+ return _.sortBy(tags).map(tag => TagElement({ tagName: tag }))
+ } else {
+ return tags.map(tag => TagElement({ tagName: tag }))
+ }
}
/**
@@ -55,7 +58,8 @@ const NoteItem = ({
pathname,
storageName,
folderName,
- viewType
+ viewType,
+ showTagsAlphabetically
}) => (
{note.tags.length > 0
- ? TagElementList(note.tags)
+ ? TagElementList(note.tags, showTagsAlphabetically)
:
(
{storageList.length > 0 ? storageList : (
-
No storage mount.
+
No storage mount.
)}
)
StorageList.propTypes = {
- storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired
+ storageList: PropTypes.arrayOf(PropTypes.element).isRequired
}
export default CSSModules(StorageList, styles)
diff --git a/browser/components/TagListItem.js b/browser/components/TagListItem.js
index 6cd50c9c..eec8ab14 100644
--- a/browser/components/TagListItem.js
+++ b/browser/components/TagListItem.js
@@ -14,8 +14,8 @@ import CSSModules from 'browser/lib/CSSModules'
* @param {bool} isRelated
*/
-const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
-
+const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => (
+
handleContextMenu(e, name)}>
{isRelated
?
diff --git a/browser/components/TodoListPercentage.js b/browser/components/TodoListPercentage.js
index 3565f274..b917bbc1 100644
--- a/browser/components/TodoListPercentage.js
+++ b/browser/components/TodoListPercentage.js
@@ -12,7 +12,7 @@ import styles from './TodoListPercentage.styl'
*/
const TodoListPercentage = ({
- percentageOfTodo
+ percentageOfTodo, onClearCheckboxClick
}) => (
@@ -20,11 +20,15 @@ const TodoListPercentage = ({
{percentageOfTodo}%
+
+
onClearCheckboxClick(e)}>clear
+
)
TodoListPercentage.propTypes = {
- percentageOfTodo: PropTypes.number.isRequired
+ percentageOfTodo: PropTypes.number.isRequired,
+ onClearCheckboxClick: PropTypes.func.isRequired
}
export default CSSModules(TodoListPercentage, styles)
diff --git a/browser/components/TodoListPercentage.styl b/browser/components/TodoListPercentage.styl
index 7b6a7d61..5a0f3257 100644
--- a/browser/components/TodoListPercentage.styl
+++ b/browser/components/TodoListPercentage.styl
@@ -1,4 +1,5 @@
.percentageBar
+ display: flex
position absolute
top 72px
right 0px
@@ -30,6 +31,20 @@
color #f4f4f4
font-weight 600
+.todoClear
+ display flex
+ justify-content: flex-end
+ position absolute
+ z-index 120
+ width 100%
+ height 100%
+ padding 2px 10px
+
+.todoClearText
+ color #f4f4f4
+ cursor pointer
+ font-weight 500
+
body[data-theme="dark"]
.percentageBar
background-color #444444
@@ -39,6 +54,9 @@ body[data-theme="dark"]
.percentageText
color $ui-dark-text-color
+
+ .todoClearText
+ color $ui-dark-text-color
body[data-theme="solarized-dark"]
.percentageBar
@@ -50,6 +68,9 @@ body[data-theme="solarized-dark"]
.percentageText
color #fdf6e3
+ .todoClearText
+ color #fdf6e3
+
body[data-theme="monokai"]
.percentageBar
background-color: $ui-monokai-borderColor
@@ -69,3 +90,6 @@ body[data-theme="dracula"]
.percentageText
color $ui-dracula-text-color
+
+ .percentageText
+ color $ui-dracula-text-color
diff --git a/browser/lib/Languages.js b/browser/lib/Languages.js
index ddb7e0ed..1a798fdf 100644
--- a/browser/lib/Languages.js
+++ b/browser/lib/Languages.js
@@ -49,7 +49,7 @@ const languages = [
},
{
name: 'Portuguese',
- locale: 'pt'
+ locale: 'pt-BR'
},
{
name: 'Russian',
@@ -61,6 +61,9 @@ const languages = [
}, {
name: 'Turkish',
locale: 'tr'
+ }, {
+ name: 'Thai',
+ locale: 'th'
}
]
diff --git a/browser/lib/findNoteTitle.js b/browser/lib/findNoteTitle.js
index b954f172..912c3bdd 100644
--- a/browser/lib/findNoteTitle.js
+++ b/browser/lib/findNoteTitle.js
@@ -1,4 +1,4 @@
-export function findNoteTitle (value) {
+export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') {
const splitted = value.split('\n')
let title = null
let isInsideCodeBlock = false
@@ -6,6 +6,11 @@ export function findNoteTitle (value) {
if (splitted[0] === '---') {
let line = 0
while (++line < splitted.length) {
+ if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) {
+ title = splitted[line].substring(frontMatterTitleField.length + 1).trim()
+
+ break
+ }
if (splitted[line] === '---') {
splitted.splice(0, line + 1)
@@ -14,17 +19,19 @@ export function findNoteTitle (value) {
}
}
- splitted.some((line, index) => {
- const trimmedLine = line.trim()
- const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
- if (trimmedLine.match('```')) {
- isInsideCodeBlock = !isInsideCodeBlock
- }
- if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
- title = trimmedLine
- return true
- }
- })
+ if (title === null) {
+ splitted.some((line, index) => {
+ const trimmedLine = line.trim()
+ const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
+ if (trimmedLine.match('```')) {
+ isInsideCodeBlock = !isInsideCodeBlock
+ }
+ if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
+ title = trimmedLine
+ return true
+ }
+ })
+ }
if (title === null) {
title = ''
diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js
index fb7267ee..8f6d86a8 100644
--- a/browser/lib/markdown-it-sanitize-html.js
+++ b/browser/lib/markdown-it-sanitize-html.js
@@ -2,6 +2,7 @@
import sanitizeHtml from 'sanitize-html'
import { escapeHtmlCharacters } from './utils'
+import url from 'url'
module.exports = function sanitizePlugin (md, options) {
options = options || {}
@@ -25,7 +26,7 @@ module.exports = function sanitizePlugin (md, options) {
const inlineTokens = state.tokens[tokenIdx].children
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
if (inlineTokens[childIdx].type === 'html_inline') {
- inlineTokens[childIdx].content = sanitizeHtml(
+ inlineTokens[childIdx].content = sanitizeInline(
inlineTokens[childIdx].content,
options
)
@@ -35,3 +36,89 @@ module.exports = function sanitizePlugin (md, options) {
}
})
}
+
+const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
+const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig
+
+function sanitizeInline (html, options) {
+ let match = tagRegex.exec(html)
+ if (!match) {
+ return ''
+ }
+
+ const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options
+
+ if (match[1] !== undefined) {
+ // opening tag
+ const tag = match[1].toLowerCase()
+ if (allowedTags.indexOf(tag) === -1) {
+ return ''
+ }
+
+ const attributes = match[2]
+
+ let attrs = ''
+ let name
+ let value
+
+ while ((match = attributesRegex.exec(attributes))) {
+ name = match[1].toLowerCase()
+ value = match[3]
+
+ if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
+ if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
+ if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) {
+ continue
+ }
+ }
+
+ attrs += ` ${name}`
+ if (match[2]) {
+ attrs += `="${value}"`
+ }
+ }
+ }
+
+ if (selfClosing.indexOf(tag) === -1) {
+ return '<' + tag + attrs + '>'
+ } else {
+ return '<' + tag + attrs + ' />'
+ }
+ } else {
+ // closing tag
+ if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) {
+ return html
+ } else {
+ return ''
+ }
+ }
+}
+
+function naughtyHRef (href, options) {
+ // href = href.replace(/[\x00-\x20]+/g, '')
+ href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
+
+ const matches = href.match(/^([a-zA-Z]+)\:/)
+ if (!matches) {
+ if (href.match(/^[\/\\]{2}/)) {
+ return !options.allowProtocolRelative
+ }
+
+ // No scheme
+ return false
+ }
+
+ const scheme = matches[1].toLowerCase()
+
+ return options.allowedSchemes.indexOf(scheme) === -1
+}
+
+function naughtyIFrame (src, options) {
+ try {
+ const parsed = url.parse(src, false, true)
+
+ return options.allowedIframeHostnames.index(parsed.hostname) === -1
+ } catch (e) {
+ return true
+ }
+}
diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js
index dd8d429e..be04fb08 100644
--- a/browser/lib/markdown.js
+++ b/browser/lib/markdown.js
@@ -21,7 +21,7 @@ function createGutter (str, firstLineNumber) {
class Markdown {
constructor (options = {}) {
- let config = ConfigManager.get()
+ const config = ConfigManager.get()
const defaultOptions = {
typographer: config.preview.smartQuotes,
linkify: true,
@@ -80,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
})
}
@@ -276,9 +280,6 @@ class Markdown {
}
// FIXME We should not depend on global variable.
window.md = this.md
- this.updateConfig = () => {
- config = ConfigManager.get()
- }
}
render (content) {
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index e4493a80..116fdec0 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -61,11 +61,14 @@ class MarkdownNoteDetail extends React.Component {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType)
})
+ ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
ee.on('code:generate-toc', this.generateToc)
}
componentWillReceiveProps (nextProps) {
- if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
+ const isNewNote = nextProps.note.key !== this.props.note.key
+ const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length
+ if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow()
this.setState({
note: Object.assign({}, nextProps.note)
@@ -91,7 +94,7 @@ class MarkdownNoteDetail extends React.Component {
handleUpdateContent () {
const { note } = this.state
note.content = this.refs.content.value
- note.title = markdown.strip(striptags(findNoteTitle(note.content)))
+ note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
this.updateNote(note)
}
@@ -293,9 +296,33 @@ class MarkdownNoteDetail extends React.Component {
})
}
+ handleDeleteNote () {
+ this.handleTrashButtonClick()
+ }
+
+ handleClearTodo () {
+ const { note } = this.state
+ const splitted = note.content.split('\n')
+
+ const clearTodoContent = splitted.map((line) => {
+ const trimmedLine = line.trim()
+ if (trimmedLine.match(/\[x\]/i)) {
+ return line.replace(/\[x\]/i, '[ ]')
+ } else {
+ return line
+ }
+ }).join('\n')
+
+ note.content = clearTodoContent
+ this.refs.content.setValue(note.content)
+
+ this.updateNote(note)
+ }
+
renderEditor () {
const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state
+
if (this.state.editorType === 'EDITOR_PREVIEW') {
return
-
+ this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
this.handleSwitchMode(e)} editorType={editorType} />
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index 9356a02c..afd81102 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -112,7 +112,7 @@ class SnippetNoteDetail extends React.Component {
if (this.refs.tags) note.tags = this.refs.tags.value
note.description = this.refs.description.value
note.updatedAt = new Date()
- note.title = findNoteTitle(note.description)
+ note.title = findNoteTitle(note.description, false)
this.setState({
note
@@ -354,12 +354,10 @@ class SnippetNoteDetail extends React.Component {
this.refs['code-' + this.state.snippetIndex].reload()
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
- console.log('no need for arrows')
this.moveTabBarBy(0)
} else {
const lastTab = this.allTabs.lastChild
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
- console.log('need to scroll')
const width = this.visibleTabs.offsetWidth
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
@@ -627,7 +625,6 @@ class SnippetNoteDetail extends React.Component {
}
focusEditor () {
- console.log('code-' + this.state.snippetIndex)
this.refs['code-' + this.state.snippetIndex].focus()
}
@@ -759,6 +756,8 @@ class SnippetNoteDetail extends React.Component {
this.handleChange(e)}
/>
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js
index eb160e4c..6ced475b 100644
--- a/browser/main/Detail/TagSelect.js
+++ b/browser/main/Detail/TagSelect.js
@@ -179,10 +179,10 @@ class TagSelect extends React.Component {
}
render () {
- const { value, className } = this.props
+ const { value, className, showTagsAlphabetically } = this.props
const tagList = _.isArray(value)
- ? value.map((tag) => {
+ ? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
return (
(
-
-
onClick('SPLIT')}>
-

-
-
onClick('EDITOR_PREVIEW')}>
-

-
-
{i18n.__('Toggle Mode')}
-
-)
-
-ToggleModeButton.propTypes = {
- onClick: PropTypes.func.isRequired,
- editorType: PropTypes.string.Required
-}
-
-export default CSSModules(ToggleModeButton, styles)
+import PropTypes from 'prop-types'
+import React from 'react'
+import CSSModules from 'browser/lib/CSSModules'
+import styles from './ToggleModeButton.styl'
+import i18n from 'browser/lib/i18n'
+
+const ToggleModeButton = ({
+ onClick, editorType
+}) => (
+
+
onClick('SPLIT')}>
+

+
+
onClick('EDITOR_PREVIEW')}>
+

+
+
{i18n.__('Toggle Mode')}
+
+)
+
+ToggleModeButton.propTypes = {
+ onClick: PropTypes.func.isRequired,
+ editorType: PropTypes.string.Required
+}
+
+export default CSSModules(ToggleModeButton, styles)
diff --git a/browser/main/Main.js b/browser/main/Main.js
index 5fe9d493..e9d2c94d 100644
--- a/browser/main/Main.js
+++ b/browser/main/Main.js
@@ -80,7 +80,6 @@ class Main extends React.Component {
}
})
.then(data => {
- console.log(data)
store.dispatch({
type: 'ADD_STORAGE',
storage: data.storage,
@@ -297,7 +296,7 @@ class Main extends React.Component {
onMouseUp={e => this.handleMouseUp(e)}
>
{!config.isSideNavFolded &&
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index 30ad93c3..13117af1 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -56,7 +56,6 @@ class NoteList extends React.Component {
super(props)
this.selectNextNoteHandler = () => {
- console.log('fired next')
this.selectNextNote()
}
this.selectPriorNoteHandler = () => {
@@ -616,7 +615,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 +634,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 +993,7 @@ class NoteList extends React.Component {
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
+ showTagsAlphabetically={config.ui.showTagsAlphabetically}
/>
)
}
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index 977a8fb5..3e18095e 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -18,6 +18,11 @@ 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'
+
+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 +35,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 +95,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 +206,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 +235,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}
@@ -247,7 +318,6 @@ class SideNav extends React.Component {
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
- console.log('Trash emptied')
}
handleFilterButtonContextMenu (event) {
diff --git a/browser/main/lib/AwsMobileAnalyticsConfig.js b/browser/main/lib/AwsMobileAnalyticsConfig.js
index 1ef4f8da..e4a21a92 100644
--- a/browser/main/lib/AwsMobileAnalyticsConfig.js
+++ b/browser/main/lib/AwsMobileAnalyticsConfig.js
@@ -45,7 +45,6 @@ function initAwsMobileAnalytics () {
if (getSendEventCond()) return
AWS.config.credentials.get((err) => {
if (!err) {
- console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
recordDynamicCustomEvent('APP_STARTED')
recordStaticCustomEvent()
}
@@ -58,7 +57,7 @@ function recordDynamicCustomEvent (type, options = {}) {
mobileAnalyticsClient.recordEvent(type, options)
} catch (analyticsError) {
if (analyticsError instanceof ReferenceError) {
- console.log(analyticsError.name + ': ' + analyticsError.message)
+ console.error(analyticsError.name + ': ' + analyticsError.message)
}
}
}
@@ -71,7 +70,7 @@ function recordStaticCustomEvent () {
})
} catch (analyticsError) {
if (analyticsError instanceof ReferenceError) {
- console.log(analyticsError.name + ': ' + analyticsError.message)
+ console.error(analyticsError.name + ': ' + analyticsError.message)
}
}
}
diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js
index 1727deb8..9611f21d 100644
--- a/browser/main/lib/ConfigManager.js
+++ b/browser/main/lib/ConfigManager.js
@@ -24,7 +24,8 @@ export const DEFAULT_CONFIG = {
amaEnabled: true,
hotkey: {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
- toggleMode: OSX ? 'Command + Option + M' : 'Ctrl + M'
+ toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
+ deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
},
ui: {
language: 'en',
@@ -43,11 +44,14 @@ 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'
},
preview: {
fontSize: '14',
@@ -60,6 +64,7 @@ export const DEFAULT_CONFIG = {
latexBlockClose: '$$',
plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
scrollPastEnd: false,
+ scrollSync: true,
smartQuotes: true,
breaks: true,
smartArrows: false,
diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js
index 912450c1..c193eaf2 100644
--- a/browser/main/lib/dataApi/attachmentManagement.js
+++ b/browser/main/lib/dataApi/attachmentManagement.js
@@ -529,7 +529,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
return modifiedLinkText
})
} else {
- console.log('One if the parameters was null -> Do nothing..')
return Promise.resolve(linkText)
}
}
diff --git a/browser/main/lib/dataApi/renameStorage.js b/browser/main/lib/dataApi/renameStorage.js
index 78242bed..3b806d1c 100644
--- a/browser/main/lib/dataApi/renameStorage.js
+++ b/browser/main/lib/dataApi/renameStorage.js
@@ -14,7 +14,6 @@ function renameStorage (key, name) {
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
} catch (err) {
- console.log('error got')
console.error(err)
return Promise.reject(err)
}
diff --git a/browser/main/lib/dataApi/resolveStorageData.js b/browser/main/lib/dataApi/resolveStorageData.js
index 681a102e..da41f3d0 100644
--- a/browser/main/lib/dataApi/resolveStorageData.js
+++ b/browser/main/lib/dataApi/resolveStorageData.js
@@ -31,13 +31,9 @@ function resolveStorageData (storageCache) {
const version = parseInt(storage.version, 10)
if (version >= 1) {
- if (version > 1) {
- console.log('The repository version is newer than one of current app.')
- }
return Promise.resolve(storage)
}
- console.log('Transform Legacy storage', storage.path)
return migrateFromV6Storage(storage.path)
.then(() => storage)
}
diff --git a/browser/main/lib/dataApi/resolveStorageNotes.js b/browser/main/lib/dataApi/resolveStorageNotes.js
index fa3f19ae..9da27248 100644
--- a/browser/main/lib/dataApi/resolveStorageNotes.js
+++ b/browser/main/lib/dataApi/resolveStorageNotes.js
@@ -9,7 +9,7 @@ function resolveStorageNotes (storage) {
notePathList = sander.readdirSync(notesDirPath)
} catch (err) {
if (err.code === 'ENOENT') {
- console.log(notesDirPath, ' doesn\'t exist.')
+ console.error(notesDirPath, ' doesn\'t exist.')
sander.mkdirSync(notesDirPath)
} else {
console.warn('Failed to find note dir', notesDirPath, err)
diff --git a/browser/main/lib/dataApi/toggleStorage.js b/browser/main/lib/dataApi/toggleStorage.js
index dbb625c3..246d85ef 100644
--- a/browser/main/lib/dataApi/toggleStorage.js
+++ b/browser/main/lib/dataApi/toggleStorage.js
@@ -12,7 +12,6 @@ function toggleStorage (key, isOpen) {
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
} catch (err) {
- console.log('error got')
console.error(err)
return Promise.reject(err)
}
diff --git a/browser/main/lib/eventEmitter.js b/browser/main/lib/eventEmitter.js
index de08f078..1276545b 100644
--- a/browser/main/lib/eventEmitter.js
+++ b/browser/main/lib/eventEmitter.js
@@ -14,7 +14,6 @@ function once (name, listener) {
}
function emit (name, ...args) {
- console.log(name)
remote.getCurrentWindow().webContents.send(name, ...args)
}
diff --git a/browser/main/lib/ipcClient.js b/browser/main/lib/ipcClient.js
index 0c916617..c06296b5 100644
--- a/browser/main/lib/ipcClient.js
+++ b/browser/main/lib/ipcClient.js
@@ -14,14 +14,13 @@ nodeIpc.connectTo(
path.join(app.getPath('userData'), 'boostnote.service'),
function () {
nodeIpc.of.node.on('error', function (err) {
- console.log(err)
+ console.error(err)
})
nodeIpc.of.node.on('connect', function () {
- console.log('Connected successfully')
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
})
nodeIpc.of.node.on('disconnect', function () {
- console.log('disconnected')
+ return
})
}
)
diff --git a/browser/main/lib/shortcut.js b/browser/main/lib/shortcut.js
index a6f33196..93e33c9b 100644
--- a/browser/main/lib/shortcut.js
+++ b/browser/main/lib/shortcut.js
@@ -3,5 +3,8 @@ import ee from 'browser/main/lib/eventEmitter'
module.exports = {
'toggleMode': () => {
ee.emit('topbar:togglemodebutton')
+ },
+ 'deleteNote': () => {
+ ee.emit('hotkey:deletenote')
}
}
diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.js b/browser/main/modals/PreferencesModal/Crowdfunding.js
index f342fb76..f6389cd8 100644
--- a/browser/main/modals/PreferencesModal/Crowdfunding.js
+++ b/browser/main/modals/PreferencesModal/Crowdfunding.js
@@ -23,21 +23,29 @@ class Crowdfunding extends React.Component {
return (
{i18n.__('Crowdfunding')}
-
{i18n.__('Dear Boostnote users,')}
-
{i18n.__('Thank you for using Boostnote!')}
-
{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}
-
{i18n.__('To support our growing userbase, and satisfy community expectations,')}
-
{i18n.__('we would like to invest more time and resources in this project.')}
+
{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}
+
{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}
-
{i18n.__('If you use Boostnote and see its potential, help us out by supporting the project on OpenCollective!')}
+
{i18n.__('### Sustainable Open Source Ecosystem')}
+
{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}
+
{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}
+
{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}
-
{i18n.__('Thanks,')}
+
{i18n.__('### We believe Meritocracy')}
+
{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}
+
{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}
+
{i18n.__('It sometimes looks like exploitation.')}
+
{i18n.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}
+
+
{i18n.__('As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.')}
+
+
{i18n.__('Thank you,')}
{i18n.__('The Boostnote Team')}
- this.handleLinkClick(e)}>{i18n.__('Support via OpenCollective')}
+ this.handleLinkClick(e)}>{i18n.__('See IssueHunt')}
)
diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js
index 1c40a13a..7ad6f606 100644
--- a/browser/main/modals/PreferencesModal/HotkeyTab.js
+++ b/browser/main/modals/PreferencesModal/HotkeyTab.js
@@ -28,10 +28,20 @@ class HotkeyTab extends React.Component {
}})
}
this.handleSettingError = (err) => {
- this.setState({keymapAlert: {
- type: 'error',
- message: err.message != null ? err.message : i18n.__('An error occurred!')
- }})
+ if (
+ this.state.config.hotkey.toggleMain === '' ||
+ this.state.config.hotkey.toggleMode === ''
+ ) {
+ this.setState({keymapAlert: {
+ type: 'success',
+ message: i18n.__('Successfully applied!')
+ }})
+ } else {
+ this.setState({keymapAlert: {
+ type: 'error',
+ message: err.message != null ? err.message : i18n.__('An error occurred!')
+ }})
+ }
}
this.oldHotkey = this.state.config.hotkey
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
@@ -68,7 +78,8 @@ class HotkeyTab extends React.Component {
const { config } = this.state
config.hotkey = {
toggleMain: this.refs.toggleMain.value,
- toggleMode: this.refs.toggleMode.value
+ toggleMode: this.refs.toggleMode.value,
+ deleteNote: this.refs.deleteNote.value
}
this.setState({
config
@@ -127,6 +138,17 @@ class HotkeyTab extends React.Component {
/>
+
+
{i18n.__('Delete Note')}
+
+ this.handleHotkeyChange(e)}
+ ref='deleteNote'
+ value={config.hotkey.deleteNote}
+ type='text'
+ />
+
+
this.handleHintToggleButtonClick(e)}
diff --git a/browser/main/modals/PreferencesModal/InfoTab.js b/browser/main/modals/PreferencesModal/InfoTab.js
index 1b2d55bb..a6acc963 100644
--- a/browser/main/modals/PreferencesModal/InfoTab.js
+++ b/browser/main/modals/PreferencesModal/InfoTab.js
@@ -84,7 +84,7 @@ class InfoTab extends React.Component {
>{i18n.__('GitHub')}
- this.handleLinkClick(e)}
>{i18n.__('Blog')}
diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js
index 97f3877c..6bc3b0a3 100644
--- a/browser/main/modals/PreferencesModal/UiTab.js
+++ b/browser/main/modals/PreferencesModal/UiTab.js
@@ -71,6 +71,9 @@ class UiTab extends React.Component {
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 +92,9 @@ 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
},
preview: {
fontSize: this.refs.previewFontSize.value,
@@ -102,6 +107,7 @@ 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,
@@ -246,16 +252,6 @@ class UiTab extends React.Component {
{i18n.__('Show a confirmation dialog when deleting notes')}
-
-
-
{
global.process.platform === 'win32'
?
@@ -271,6 +267,52 @@ class UiTab extends React.Component {
: null
}
+ Tags
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Editor
@@ -428,6 +470,31 @@ class UiTab extends React.Component {
+
+
+ {i18n.__('Front matter title field')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
+
+
+
+
+
+
+