diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js
index 600b7e2d..625bb38d 100644
--- a/browser/components/NoteItem.js
+++ b/browser/components/NoteItem.js
@@ -4,6 +4,7 @@
import PropTypes from 'prop-types'
import React from 'react'
import { isArray } from 'lodash'
+import invertColor from 'invert-color'
import CSSModules from 'browser/lib/CSSModules'
import { getTodoStatus } from 'browser/lib/getTodoStatus'
import styles from './NoteItem.styl'
@@ -13,27 +14,39 @@ import i18n from 'browser/lib/i18n'
/**
* @description Tag element component.
* @param {string} tagName
+ * @param {string} color
* @return {React.Component}
*/
-const TagElement = ({ tagName }) => (
-
-)
+const TagElement = ({ tagName, color }) => {
+ const style = {}
+ if (color) {
+ style.backgroundColor = color
+ style.color = invertColor(color, { black: '#222', white: '#f1f1f1', threshold: 0.3 })
+ }
+ return (
+
+ )
+}
/**
* @description Tag element list component.
* @param {Array|null} tags
+ * @param {boolean} showTagsAlphabetically
+ * @param {Object} coloredTags
* @return {React.Component}
*/
-const TagElementList = tags => {
+const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
if (!isArray(tags)) {
return []
}
- const tagElements = tags.map(tag => TagElement({ tagName: tag }))
-
- return tagElements
+ if (showTagsAlphabetically) {
+ return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
+ } else {
+ return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
+ }
}
/**
@@ -43,6 +56,7 @@ const TagElementList = tags => {
* @param {Function} handleNoteClick
* @param {Function} handleNoteContextMenu
* @param {Function} handleDragStart
+ * @param {Object} coloredTags
* @param {string} dateDisplay
*/
const NoteItem = ({
@@ -55,7 +69,9 @@ const NoteItem = ({
pathname,
storageName,
folderName,
- viewType
+ viewType,
+ showTagsAlphabetically,
+ coloredTags
}) => (
{note.tags.length > 0
- ? TagElementList(note.tags)
+ ? TagElementList(note.tags, showTagsAlphabetically, coloredTags)
:
(
{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 19f11791..9aa00a1d 100644
--- a/browser/components/TagListItem.js
+++ b/browser/components/TagListItem.js
@@ -10,11 +10,12 @@ import CSSModules from 'browser/lib/CSSModules'
* @param {string} name
* @param {Function} handleClickTagListItem
* @param {Function} handleClickNarrowToTag
-* @param {bool} isActive
-* @param {bool} isRelated
+* @param {boolean} isActive
+* @param {boolean} isRelated
+* @param {string} bgColor tab backgroundColor
*/
-const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => (
+const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count, color}) => (
handleContextMenu(e, name)}>
{isRelated
?
handleClickNarrowToTag(name)}>
@@ -23,9 +24,10 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, hand
:
}
handleClickTagListItem(name)}>
+
{`# ${name}`}
- {count}
+ {count !== 0 ? count : ''}
@@ -33,7 +35,8 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, hand
TagListItem.propTypes = {
name: PropTypes.string.isRequired,
- handleClickTagListItem: PropTypes.func.isRequired
+ handleClickTagListItem: PropTypes.func.isRequired,
+ color: PropTypes.string
}
export default CSSModules(TagListItem, styles)
diff --git a/browser/components/TagListItem.styl b/browser/components/TagListItem.styl
index 555520b0..9f407a17 100644
--- a/browser/components/TagListItem.styl
+++ b/browser/components/TagListItem.styl
@@ -71,6 +71,11 @@
padding-right 15px
font-size 13px
+.tagList-item-color
+ height 26px
+ width 3px
+ display inline-block
+
body[data-theme="white"]
.tagList-item
color $ui-inactive-text-color
diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl
index e091331b..4921b531 100644
--- a/browser/components/markdown.styl
+++ b/browser/components/markdown.styl
@@ -55,11 +55,14 @@ body
line-height 1.6
overflow-x hidden
background-color $ui-noteDetail-backgroundColor
+ // do not allow display line breaks
+ .katex-display > .katex
+ white-space nowrap
+ // allow inline line breaks
.katex
- font 400 1.2em 'KaTeX_Main'
- line-height 1.2em
white-space initial
- text-indent 0
+ .katex .katex-html
+ display inline-flex
.katex .mfrac>.vlist>span:nth-child(2)
top 0 !important
.katex-error
@@ -162,6 +165,7 @@ p
white-space pre-line
word-wrap break-word
img
+ cursor zoom-in
max-width 100%
strong, b
font-weight bold
@@ -183,6 +187,10 @@ ul
display list-item
&.taskListItem
list-style none
+ &>input
+ margin-left -1.6em
+ &>p
+ margin-left -1.8em
p
margin 0
&>li>ul, &>li>ol
@@ -209,41 +217,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
@@ -375,6 +381,69 @@ 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
+
+ .gallery
+ width 100%
+ height 50vh
+
+ .carousel
+ .carousel-main img
+ min-width auto
+ max-width 100%
+ min-height auto
+ max-height 100%
+
+ .carousel-footer::-webkit-scrollbar-corner
+ background-color transparent
+
+ .carousel-main, .carousel-footer
+ background-color $ui-noteDetail-backgroundColor
+ .prev, .next
+ color $ui-text-color
+ background-color $ui-tag-backgroundColor
+
themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -425,6 +494,22 @@ 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
+
+ pre.fence
+ .gallery
+ .carousel-main, .carousel-footer
+ background-color $ui-dark-noteDetail-backgroundColor
+ .prev, .next
+ color $ui-dark-text-color
+ background-color $ui-dark-tag-backgroundColor
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
@@ -452,6 +537,22 @@ 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
+
+ pre.fence
+ .gallery
+ .carousel-main, .carousel-footer
+ background-color $ui-solarized-dark-noteDetail-backgroundColor
+ .prev, .next
+ color $ui-solarized-dark-button--active-color
+ background-color $ui-solarized-dark-button-backgroundColor
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
@@ -482,6 +583,23 @@ body[data-theme="monokai"]
kbd
background-color themeDarkBackground
+ dl
+ border-color themeDarkBorder
+ background-color themeMonokaiTableHead
+ dt
+ border-color themeDarkBorder
+ dd
+ border-color themeDarkBorder
+ background-color $ui-monokai-noteDetail-backgroundColor
+
+ pre.fence
+ .gallery
+ .carousel-main, .carousel-footer
+ background-color $ui-monokai-noteDetail-backgroundColor
+ .prev, .next
+ color $ui-monokai-button--active-color
+ background-color $ui-monokai-button-backgroundColor
+
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
themeDraculaTableHead = themeDraculaTableEven
@@ -509,4 +627,21 @@ body[data-theme="dracula"]
&:last-child
border-right solid 1px themeDraculaTableBorder
kbd
- background-color themeDarkBackground
\ No newline at end of file
+ background-color themeDarkBackground
+
+ dl
+ border-color themeDarkBorder
+ background-color themeDraculaTableHead
+ dt
+ border-color themeDarkBorder
+ dd
+ border-color themeDarkBorder
+ background-color $ui-dracula-noteDetail-backgroundColor
+
+ pre.fence
+ .gallery
+ .carousel-main, .carousel-footer
+ background-color $ui-dracula-noteDetail-backgroundColor
+ .prev, .next
+ color $ui-dracula-button--active-color
+ background-color $ui-dracula-button-backgroundColor
diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js
index e8784d9d..e28e06ea 100644
--- a/browser/components/render/MermaidRender.js
+++ b/browser/components/render/MermaidRender.js
@@ -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' || theme === 'dracula'
+ 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
}
diff --git a/browser/lib/Languages.js b/browser/lib/Languages.js
index ddb7e0ed..8c3747a9 100644
--- a/browser/lib/Languages.js
+++ b/browser/lib/Languages.js
@@ -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'
}
]
diff --git a/browser/lib/contextMenuBuilder.js b/browser/lib/contextMenuBuilder.js
new file mode 100644
index 00000000..cf92f52e
--- /dev/null
+++ b/browser/lib/contextMenuBuilder.js
@@ -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
diff --git a/browser/lib/markdown-it-deflist.js b/browser/lib/markdown-it-deflist.js
new file mode 100644
index 00000000..db14c636
--- /dev/null
+++ b/browser/lib/markdown-it-deflist.js
@@ -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' ] })
+}
diff --git a/browser/lib/markdown-it-fence.js b/browser/lib/markdown-it-fence.js
new file mode 100644
index 00000000..f2f7e999
--- /dev/null
+++ b/browser/lib/markdown-it-fence.js
@@ -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])
+ }
+}
diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js
index 05e5e7be..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 || {}
@@ -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
+ }
+}
diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js
index 716be83a..af1c833f 100644
--- a/browser/lib/markdown-toc-generator.js
+++ b/browser/lib/markdown-toc-generator.js
@@ -2,44 +2,27 @@
* @fileoverview Markdown table of contents generator
*/
+import { EOL } from 'os'
import toc from 'markdown-toc'
-import diacritics from 'diacritics-map'
-import stripColor from 'strip-color'
+import mdlink from 'markdown-link'
+import slugify from './slugify'
-const EOL = require('os').EOL
+const hasProp = Object.prototype.hasOwnProperty
/**
- * @caseSensitiveSlugify Custom slugify function
- * Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js),
- * but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067
+ * From @enyaxu/markdown-it-anchor
*/
-function caseSensitiveSlugify (str) {
- function replaceDiacritics (str) {
- return str.replace(/[À-ž]/g, function (ch) {
- return diacritics[ch] || ch
- })
- }
+function uniqueSlug (slug, slugs, opts) {
+ let uniq = slug
+ let i = opts.uniqueSlugStartIndex
+ while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
+ slugs[uniq] = true
+ return uniq
+}
- function getTitle (str) {
- if (/^\[[^\]]+\]\(/.test(str)) {
- var m = /^\[([^\]]+)\]/.exec(str)
- if (m) return m[1]
- }
- return str
- }
-
- str = getTitle(str)
- str = stripColor(str)
- // str = str.toLowerCase() //let's be case sensitive
-
- // `.split()` is often (but not always) faster than `.replace()`
- str = str.split(' ').join('-')
- str = str.split(/\t/).join('--')
- str = str.split(/<\/?[^>]+>/).join('')
- str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('')
- str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('')
- str = replaceDiacritics(str)
- return str
+function linkify (token) {
+ token.content = mdlink(token.content, '#' + token.slug)
+ return token
}
const TOC_MARKER_START = ''
@@ -84,8 +67,23 @@ export function generateInEditor (editor) {
* @returns generatedTOC String containing generated TOC
*/
export function generate (markdownText) {
- const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify})
- return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END
+ const slugs = {}
+ const opts = {
+ uniqueSlugStartIndex: 1
+ }
+
+ const result = toc(markdownText, {
+ slugify: title => {
+ return uniqueSlug(slugify(title), slugs, opts)
+ },
+ linkify: false
+ })
+
+ const md = toc.bullets(result.json.map(linkify), {
+ highest: result.highest
+ })
+
+ return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
}
function wrapTocWithEol (toc, editor) {
diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js
index 49260740..0ea15ba9 100644
--- a/browser/lib/markdown.js
+++ b/browser/lib/markdown.js
@@ -7,7 +7,6 @@ import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
import { lastFindInArray } from './utils'
-import ee from 'browser/main/lib/eventEmitter'
function createGutter (str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
@@ -28,32 +27,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 `${str} `
- }
- if (langType === 'sequence') {
- return `${str} `
- }
- if (langType === 'chart') {
- return `${str} `
- }
- if (langType === 'mermaid') {
- return `${str} `
- }
- return '' +
- '' + fileName + ' ' +
- createGutter(str, firstLineNumber) +
- '' +
- str +
- ' '
- },
sanitize: 'STRICT'
}
@@ -106,7 +79,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
})
}
@@ -140,19 +117,69 @@ 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()
- .replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
- .replace(/\s+/g, '-'))
- .replace(/\-+$/, '')
- }
+ this.md.use(require('@enyaxu/markdown-it-anchor'), {
+ slugify: require('./slugify')
})
this.md.use(require('markdown-it-kbd'))
-
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
+ this.md.use(require('markdown-it-abbr'))
+ this.md.use(require('markdown-it-sub'))
+ this.md.use(require('markdown-it-sup'))
+ this.md.use(require('./markdown-it-deflist'))
this.md.use(require('./markdown-it-frontmatter'))
+ this.md.use(require('./markdown-it-fence'), {
+ chart: token => {
+ if (token.parameters.hasOwnProperty('yaml')) {
+ token.parameters.format = 'yaml'
+ }
+
+ return `
+ ${token.fileName}
+ ${token.content}
+ `
+ },
+ flowchart: token => {
+ return `
+ ${token.fileName}
+ ${token.content}
+ `
+ },
+ gallery: token => {
+ const content = token.content.split('\n').slice(0, -1).map(line => {
+ const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
+ if (match) {
+ return match[1]
+ } else {
+ return line
+ }
+ }).join('\n')
+
+ return `
+ ${token.fileName}
+ ${content}
+ `
+ },
+ mermaid: token => {
+ return `
+ ${token.fileName}
+ ${token.content}
+ `
+ },
+ sequence: token => {
+ return `
+ ${token.fileName}
+ ${token.content}
+ `
+ }
+ }, token => {
+ return `
+ ${token.fileName}
+ ${createGutter(token.content, token.firstLineNumber)}
+ ${token.content}
+ `
+ })
+
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) {
@@ -253,9 +280,12 @@ class Markdown {
this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => {
switch (token.type) {
- case 'heading_open':
- case 'paragraph_open':
case 'blockquote_open':
+ case 'dd_open':
+ case 'dt_open':
+ case 'heading_open':
+ case 'list_item_open':
+ case 'paragraph_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
diff --git a/browser/lib/markdownTextHelper.js b/browser/lib/markdownTextHelper.js
index 1501e2c7..1657efd9 100644
--- a/browser/lib/markdownTextHelper.js
+++ b/browser/lib/markdownTextHelper.js
@@ -22,7 +22,7 @@ export function strip (input) {
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
.replace(/>/g, '')
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
- .replace(/^#{1,6}\s*([^#]*)\s*(#{1,6})?/gm, '$1')
+ .replace(/^#{1,6}\s*/gm, '')
.replace(/(`{3,})(.*?)\1/gm, '$2')
.replace(/^-{3,}\s*$/g, '')
.replace(/`(.+?)`/g, '$1')
diff --git a/browser/lib/newNote.js b/browser/lib/newNote.js
index bed69735..d8ef196f 100644
--- a/browser/lib/newNote.js
+++ b/browser/lib/newNote.js
@@ -3,15 +3,23 @@ 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: '',
- content: ''
+ tags,
+ content: '',
+ linesHighlighted: []
})
.then(note => {
const noteHash = note.key
@@ -29,20 +37,30 @@ 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(' ')
+ }
+
+ const defaultLanguage = config.editor.snippetDefaultLanguage === 'Auto Detect' ? null : config.editor.snippetDefaultLanguage
+
return dataApi
.createNote(storage, {
type: 'SNIPPET_NOTE',
folder: folder,
title: '',
+ tags,
description: '',
snippets: [
{
name: '',
- mode: config.editor.snippetDefaultLanguage || 'text',
- content: ''
+ mode: defaultLanguage,
+ content: '',
+ linesHighlighted: []
}
]
})
diff --git a/browser/lib/slugify.js b/browser/lib/slugify.js
new file mode 100644
index 00000000..a3447a90
--- /dev/null
+++ b/browser/lib/slugify.js
@@ -0,0 +1,17 @@
+import diacritics from 'diacritics-map'
+
+function replaceDiacritics (str) {
+ return str.replace(/[À-ž]/g, function (ch) {
+ return diacritics[ch] || ch
+ })
+}
+
+module.exports = function slugify (title) {
+ let slug = title.trim()
+
+ slug = replaceDiacritics(slug)
+
+ slug = slug.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
+
+ return encodeURI(slug).replace(/\-+$/, '')
+}
diff --git a/browser/lib/spellcheck.js b/browser/lib/spellcheck.js
new file mode 100644
index 00000000..dd04e575
--- /dev/null
+++ b/browser/lib/spellcheck.js
@@ -0,0 +1,232 @@
+import styles from '../components/CodeEditor.styl'
+import i18n from 'browser/lib/i18n'
+
+const Typo = require('typo-js')
+const _ = require('lodash')
+
+const CSS_ERROR_CLASS = 'codeEditor-typo'
+const SPELLCHECK_DISABLED = 'NONE'
+const DICTIONARY_PATH = '../dictionaries'
+const MILLISECONDS_TILL_LIVECHECK = 500
+
+let dictionary = null
+let self
+
+function getAvailableDictionaries () {
+ return [
+ {label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED},
+ {label: i18n.__('English'), value: 'en_GB'},
+ {label: i18n.__('German'), value: 'de_DE'},
+ {label: i18n.__('French'), value: 'fr_FR'}
+ ]
+}
+
+/**
+ * Only to be used in the tests :)
+ */
+function setDictionaryForTestsOnly (newDictionary) {
+ dictionary = newDictionary
+}
+
+/**
+ * @description Initializes the spellcheck. It removes all existing marks of the current editor.
+ * If a language was given (i.e. lang !== this.SPELLCHECK_DISABLED) it will load the stated dictionary and use it to check the whole document.
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param {String} lang on of the values from getAvailableDictionaries()-Method
+ */
+function setLanguage (editor, lang) {
+ self = this
+ dictionary = null
+
+ if (editor == null) {
+ return
+ }
+
+ const existingMarks = editor.getAllMarks() || []
+ for (const mark of existingMarks) {
+ mark.clear()
+ }
+ if (lang !== SPELLCHECK_DISABLED) {
+ dictionary = new Typo(lang, false, false, {
+ dictionaryPath: DICTIONARY_PATH,
+ asyncLoad: true,
+ loadedCallback: () =>
+ checkWholeDocument(editor)
+ })
+ }
+}
+
+/**
+ * Checks the whole content of the editor for typos
+ * @param {Codemirror} editor CodeMirror-Editor
+ */
+function checkWholeDocument (editor) {
+ const lastLine = editor.lineCount() - 1
+ const textOfLastLine = editor.getLine(lastLine) || ''
+ const lastChar = textOfLastLine.length
+ const from = {line: 0, ch: 0}
+ const to = {line: lastLine, ch: lastChar}
+ checkMultiLineRange(editor, from, to)
+}
+
+/**
+ * Checks the given range for typos
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param {line, ch} from starting position of the spellcheck
+ * @param {line, ch} to end position of the spellcheck
+ */
+function checkMultiLineRange (editor, from, to) {
+ function sortRange (pos1, pos2) {
+ if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) {
+ return {from: pos2, to: pos1}
+ }
+ return {from: pos1, to: pos2}
+ }
+
+ const {from: smallerPos, to: higherPos} = sortRange(from, to)
+ for (let l = smallerPos.line; l <= higherPos.line; l++) {
+ const line = editor.getLine(l) || ''
+ let w = 0
+ if (l === smallerPos.line) {
+ w = smallerPos.ch
+ }
+ let wEnd = line.length
+ if (l === higherPos.line) {
+ wEnd = higherPos.ch
+ }
+ while (w <= wEnd) {
+ const wordRange = editor.findWordAt({line: l, ch: w})
+ self.checkWord(editor, wordRange)
+ w += (wordRange.head.ch - wordRange.anchor.ch) + 1
+ }
+ }
+}
+
+/**
+ * @description Checks whether a certain range of characters in the editor (i.e. a word) contains a typo.
+ * If so the ranged will be marked with the class CSS_ERROR_CLASS.
+ * Note: Due to performance considerations, only words with more then 3 signs are checked.
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param wordRange Object specifying the range that should be checked.
+ * Having the following structure: {anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}
+ */
+function checkWord (editor, wordRange) {
+ const word = editor.getRange(wordRange.anchor, wordRange.head)
+ if (word == null || word.length <= 3) {
+ return
+ }
+ if (!dictionary.check(word)) {
+ editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]})
+ }
+}
+
+/**
+ * Checks the changes recently made (aka live check)
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param fromChangeObject codeMirror changeObject describing the start of the editing
+ * @param toChangeObject codeMirror changeObject describing the end of the editing
+ */
+function checkChangeRange (editor, fromChangeObject, toChangeObject) {
+ /**
+ * Calculate the smallest respectively largest position as a start, resp. end, position and return it
+ * @param start CodeMirror change object
+ * @param end CodeMirror change object
+ * @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
+ */
+ function getStartAndEnd (start, end) {
+ const possiblePositions = [start.from, start.to, end.from, end.to]
+ let smallest = start.from
+ let biggest = end.to
+ for (const currentPos of possiblePositions) {
+ if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) {
+ smallest = currentPos
+ }
+ if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) {
+ biggest = currentPos
+ }
+ }
+ return {start: smallest, end: biggest}
+ }
+
+ if (dictionary === null || editor == null) { return }
+
+ try {
+ const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject)
+
+ // Expand the range to include words after/before whitespaces
+ start.ch = Math.max(start.ch - 1, 0)
+ end.ch = end.ch + 1
+
+ // clean existing marks
+ const existingMarks = editor.findMarks(start, end) || []
+ for (const mark of existingMarks) {
+ mark.clear()
+ }
+
+ self.checkMultiLineRange(editor, start, end)
+ } catch (e) {
+ console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', e)
+ }
+}
+
+function saveLiveSpellCheckFrom (changeObject) {
+ liveSpellCheckFrom = changeObject
+}
+let liveSpellCheckFrom
+const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, {
+ 'leading': true,
+ 'trailing': false
+})
+const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, {
+ 'leading': false,
+ 'trailing': true
+})
+
+/**
+ * Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param changeObject codeMirror changeObject
+ */
+function handleChange (editor, changeObject) {
+ if (dictionary === null) {
+ return
+ }
+ debouncedSpellCheckLeading(changeObject)
+ debouncedSpellCheck(editor, liveSpellCheckFrom, changeObject)
+}
+
+/**
+ * Returns an array of spelling suggestions for the given (wrong written) word.
+ * Returns an empty array if the dictionary is null (=> spellcheck is disabled) or the given word was null
+ * @param word word to be checked
+ * @returns {String[]} Array of suggestions
+ */
+function getSpellingSuggestion (word) {
+ if (dictionary == null || word == null) {
+ return []
+ }
+ return dictionary.suggest(word)
+}
+
+/**
+ * Returns the name of the CSS class used for errors
+ */
+function getCSSClassName () {
+ return styles[CSS_ERROR_CLASS]
+}
+
+module.exports = {
+ DICTIONARY_PATH,
+ CSS_ERROR_CLASS,
+ SPELLCHECK_DISABLED,
+ getAvailableDictionaries,
+ setLanguage,
+ checkChangeRange,
+ handleChange,
+ getSpellingSuggestion,
+ checkWord,
+ checkMultiLineRange,
+ checkWholeDocument,
+ setDictionaryForTestsOnly,
+ getCSSClassName
+}
diff --git a/browser/main/Detail/FullscreenButton.js b/browser/main/Detail/FullscreenButton.js
index ee212603..bd76447c 100644
--- a/browser/main/Detail/FullscreenButton.js
+++ b/browser/main/Detail/FullscreenButton.js
@@ -5,15 +5,17 @@ import styles from './FullscreenButton.styl'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
-const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
const FullscreenButton = ({
onClick
-}) => (
- onClick(e)}>
-
- {i18n.__('Fullscreen')}({hotkey})
-
-)
+}) => {
+ const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
+ return (
+ onClick(e)}>
+
+ {i18n.__('Fullscreen')}({hotkey})
+
+ )
+}
FullscreenButton.propTypes = {
onClick: PropTypes.func.isRequired
diff --git a/browser/main/Detail/FullscreenButton.styl b/browser/main/Detail/FullscreenButton.styl
index cc1a8dff..133577f3 100644
--- a/browser/main/Detail/FullscreenButton.styl
+++ b/browser/main/Detail/FullscreenButton.styl
@@ -17,6 +17,10 @@
opacity 0
transition 0.1s
+.tooltip:lang(ja)
+ @extend .tooltip
+ right 35px
+
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
\ No newline at end of file
diff --git a/browser/main/Detail/InfoPanel.js b/browser/main/Detail/InfoPanel.js
index 4ce610fa..15535186 100644
--- a/browser/main/Detail/InfoPanel.js
+++ b/browser/main/Detail/InfoPanel.js
@@ -70,22 +70,22 @@ class InfoPanel extends React.Component {
-
exportAsMd(e)}>
+ exportAsMd(e, 'export-md')}>
{i18n.__('.md')}
- exportAsTxt(e)}>
+ exportAsTxt(e, 'export-txt')}>
{i18n.__('.txt')}
- exportAsHtml(e)}>
+ exportAsHtml(e, 'export-html')}>
{i18n.__('.html')}
- print(e)}>
+ print(e, 'print')}>
{i18n.__('Print')}
diff --git a/browser/main/Detail/InfoPanelTrashed.js b/browser/main/Detail/InfoPanelTrashed.js
index db64a284..d4c8045d 100644
--- a/browser/main/Detail/InfoPanelTrashed.js
+++ b/browser/main/Detail/InfoPanelTrashed.js
@@ -31,17 +31,17 @@ const InfoPanelTrashed = ({
-
exportAsMd(e)}>
+ exportAsMd(e, 'export-md')}>
.md
- exportAsTxt(e)}>
+ exportAsTxt(e, 'export-txt')}>
.txt
- exportAsHtml(e)}>
+ exportAsHtml(e, 'export-html')}>
.html
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index f7f18d4c..3ed61eb7 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -39,12 +39,15 @@ class MarkdownNoteDetail extends React.Component {
isMovingNote: false,
note: Object.assign({
title: '',
- content: ''
+ content: '',
+ linesHighlighted: []
}, props.note),
- isLockButtonShown: false,
+ isLockButtonShown: props.config.editor.type !== 'SPLIT',
isLocked: false,
- editorType: props.config.editor.type
+ editorType: props.config.editor.type,
+ switchPreview: props.config.editor.switchPreview
}
+
this.dispatchTimer = null
this.toggleLockButton = this.handleToggleLockButton.bind(this)
@@ -61,7 +64,11 @@ 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)
+
+ // Focus content if using blur or double click
+ if (this.state.switchPreview === 'BLUR' || this.state.switchPreview === 'DBL_CLICK') this.focus()
}
componentWillReceiveProps (nextProps) {
@@ -70,7 +77,7 @@ class MarkdownNoteDetail extends React.Component {
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow()
this.setState({
- note: Object.assign({}, nextProps.note)
+ note: Object.assign({linesHighlighted: []}, nextProps.note)
}, () => {
this.refs.content.reload()
if (this.refs.tags) this.refs.tags.reset()
@@ -93,7 +100,12 @@ class MarkdownNoteDetail extends React.Component {
handleUpdateContent () {
const { note } = this.state
note.content = this.refs.content.value
- note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
+
+ let title = findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)
+ title = striptags(title)
+ title = markdown.strip(title)
+ note.title = title
+
this.updateNote(note)
}
@@ -189,6 +201,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
@@ -261,7 +303,7 @@ class MarkdownNoteDetail extends React.Component {
handleToggleLockButton (event, noteStatus) {
// first argument event is not used
- if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
+ if (noteStatus === 'CODE') {
this.setState({isLockButtonShown: true})
} else {
this.setState({isLockButtonShown: false})
@@ -287,7 +329,8 @@ class MarkdownNoteDetail extends React.Component {
}
handleSwitchMode (type) {
- this.setState({ editorType: type }, () => {
+ // If in split mode, hide the lock button
+ this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type
@@ -295,6 +338,10 @@ class MarkdownNoteDetail extends React.Component {
})
}
+ handleDeleteNote () {
+ this.handleTrashButtonClick()
+ }
+
handleClearTodo () {
const { note } = this.state
const splitted = note.content.split('\n')
@@ -326,7 +373,9 @@ class MarkdownNoteDetail extends React.Component {
value={note.content}
storageKey={note.storage}
noteKey={note.key}
+ linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
+ isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
} else {
@@ -336,6 +385,7 @@ class MarkdownNoteDetail extends React.Component {
value={note.content}
storageKey={note.storage}
noteKey={note.key}
+ linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
@@ -343,7 +393,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
@@ -394,8 +444,11 @@ class MarkdownNoteDetail extends React.Component {
this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
@@ -451,6 +504,7 @@ class MarkdownNoteDetail extends React.Component {
this.handleKeyDown(e)}
>
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index 65d5dfd3..11d8ac2a 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -20,6 +20,7 @@ import _ from 'lodash'
import {findNoteTitle} from 'browser/lib/findNoteTitle'
import convertModeName from 'browser/lib/convertModeName'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
+import FullscreenButton from './FullscreenButton'
import TrashButton from './TrashButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton'
@@ -48,7 +49,7 @@ class SnippetNoteDetail extends React.Component {
note: Object.assign({
description: ''
}, props.note, {
- snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
+ snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
})
}
@@ -76,8 +77,9 @@ class SnippetNoteDetail extends React.Component {
const nextNote = Object.assign({
description: ''
}, nextProps.note, {
- snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
+ snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
})
+
this.setState({
snippetIndex: 0,
note: nextNote
@@ -354,12 +356,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)
@@ -412,6 +412,8 @@ class SnippetNoteDetail extends React.Component {
return (e) => {
const snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value
+ snippets[index].linesHighlighted = e.options.linesHighlighted
+
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
this.setState(state => ({
note: state.note
@@ -436,6 +438,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:
{
@@ -586,13 +600,16 @@ class SnippetNoteDetail extends React.Component {
}
addSnippet () {
- const { config } = this.props
+ const { config: { editor: { snippetDefaultLanguage } } } = this.props
const { note } = this.state
+ const defaultLanguage = snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
+
note.snippets = note.snippets.concat([{
name: '',
- mode: config.editor.snippetDefaultLanguage || 'text',
- content: ''
+ mode: defaultLanguage,
+ content: '',
+ linesHighlighted: []
}])
const snippetIndex = note.snippets.length - 1
@@ -627,7 +644,6 @@ class SnippetNoteDetail extends React.Component {
}
focusEditor () {
- console.log('code-' + this.state.snippetIndex)
this.refs['code-' + this.state.snippetIndex].focus()
}
@@ -636,11 +652,18 @@ class SnippetNoteDetail extends React.Component {
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
}
- showWarning () {
+ showWarning (e, msg) {
+ const warningMessage = (msg) => ({
+ 'export-txt': 'Text export',
+ 'export-md': 'Markdown export',
+ 'export-html': 'HTML export',
+ 'print': 'Print'
+ })[msg]
+
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Sorry!'),
- detail: i18n.__('md/text import is available only a markdown note.'),
+ detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
buttons: [i18n.__('OK')]
})
}
@@ -652,6 +675,8 @@ class SnippetNoteDetail extends React.Component {
const storageKey = note.storage
const folderKey = note.folder
+ const autoDetect = config.editor.snippetDefaultLanguage === 'Auto Detect'
+
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
@@ -676,10 +701,6 @@ class SnippetNoteDetail extends React.Component {
const viewList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index
-
- let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
- if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
-
return
this.handleCodeChange(index)(e)}
ref={'code-' + index}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
storageKey={storageKey}
/>
: this.handleCodeChange(index)(e)}
ref={'code-' + index}
+ enableSmartPaste={config.editor.enableSmartPaste}
+ hotkey={config.hotkey}
+ autoDetect={autoDetect}
/>
}
@@ -759,8 +788,11 @@ class SnippetNoteDetail extends React.Component {
this.handleChange(e)}
+ coloredTags={config.coloredTags}
/>
@@ -769,11 +801,7 @@ class SnippetNoteDetail extends React.Component {
isActive={note.isStarred}
/>
-
this.handleFullScreenButton(e)}>
-
- {i18n.__('Fullscreen')}
-
+
this.handleFullScreenButton(e)} />
this.handleTrashButtonClick(e)} />
@@ -789,7 +817,9 @@ class SnippetNoteDetail extends React.Component {
createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning}
exportAsTxt={this.showWarning}
+ exportAsHtml={this.showWarning}
type={note.type}
+ print={this.showWarning}
/>
diff --git a/browser/main/Detail/SnippetNoteDetail.styl b/browser/main/Detail/SnippetNoteDetail.styl
index e3bb31c6..1af93645 100644
--- a/browser/main/Detail/SnippetNoteDetail.styl
+++ b/browser/main/Detail/SnippetNoteDetail.styl
@@ -31,7 +31,7 @@
.tabList
absolute left right
- top 55px
+ top 70px
height 30px
display flex
background-color $ui-noteDetail-backgroundColor
@@ -57,6 +57,9 @@
.tabList .tabButton
navWhiteButtonColor()
width 30px
+ border-left 1px solid $ui-borderColor
+ border-top 1px solid $ui-borderColor
+ border-right 1px solid $ui-borderColor
.tabView
absolute left right bottom
@@ -98,17 +101,34 @@
opacity 0
transition 0.1s
-body[data-theme="white"]
+body[data-theme="white"], body[data-theme="default"]
.root
box-shadow $note-detail-box-shadow
border none
+ .tabButton
+ &:hover
+ background-color alpha($ui-button--active-backgroundColor, 20%)
+ color $ui-text-color
+ transition 0.15s
+
body[data-theme="dark"]
.root
border-left 1px solid $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
box-shadow none
+ .tabList .tabButton
+ border-color $ui-dark-borderColor
+ &:hover
+ background-color alpha($ui-dark-button--active-backgroundColor, 20%)
+
+ .tabButton
+ &:hover
+ background-color alpha($ui-dark-button--active-backgroundColor, 20%)
+ color $ui-dark-text-color
+ transition 0.15s
+
.body
background-color $ui-dark-noteDetail-backgroundColor
@@ -118,7 +138,6 @@ body[data-theme="dark"]
border 1px solid $ui-dark-borderColor
.tabList
- background-color $ui-button--active-backgroundColor
background-color $ui-dark-noteDetail-backgroundColor
.tabList .list
@@ -150,6 +169,15 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color
border 1px solid $ui-solarized-dark-borderColor
+ .tabList .tabButton
+ border-color $ui-solarized-dark-borderColor
+
+ .tabButton
+ &:hover
+ color $ui-solarized-dark-button--active-color
+ background-color $ui-solarized-dark-noteDetail-backgroundColor
+ transition 0.15s
+
.tabList
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color
@@ -167,6 +195,14 @@ body[data-theme="monokai"]
color $ui-monokai-text-color
border 1px solid $ui-monokai-borderColor
+ .tabList .tabButton
+ border-color $ui-monokai-borderColor
+
+ .tabButton
+ &:hover
+ color $ui-monokai-text-color
+ background-color $ui-monokai-noteDetail-backgroundColor
+
.tabList
background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color
@@ -184,6 +220,14 @@ body[data-theme="dracula"]
color $ui-dracula-text-color
border 1px solid $ui-dracula-borderColor
+ .tabList .tabButton
+ border-color $ui-dracula-borderColor
+
+ .tabButton
+ &:hover
+ color $ui-dracula-text-color
+ background-color $ui-dracula-noteDetail-backgroundColor
+
.tabList
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color
\ No newline at end of file
diff --git a/browser/main/Detail/StarButton.js b/browser/main/Detail/StarButton.js
index d74809cd..8000970d 100644
--- a/browser/main/Detail/StarButton.js
+++ b/browser/main/Detail/StarButton.js
@@ -54,7 +54,7 @@ class StarButton extends React.Component {
: '../resources/icon/icon-star.svg'
}
/>
-
{i18n.__('Star')}
+
{i18n.__('Star')}
)
}
diff --git a/browser/main/Detail/StarButton.styl b/browser/main/Detail/StarButton.styl
index d5fd755b..e9c523e9 100644
--- a/browser/main/Detail/StarButton.styl
+++ b/browser/main/Detail/StarButton.styl
@@ -21,6 +21,11 @@
opacity 0
transition 0.1s
+.tooltip:lang(ja)
+ @extend .tooltip
+ right 103px
+ width 70px
+
.root--active
@extend .root
transition 0.15s
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js
index eb160e4c..e3d9a567 100644
--- a/browser/main/Detail/TagSelect.js
+++ b/browser/main/Detail/TagSelect.js
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import React from 'react'
+import invertColor from 'invert-color'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl'
import _ from 'lodash'
@@ -45,8 +46,14 @@ class TagSelect extends React.Component {
value = _.isArray(value)
? value.slice()
: []
- value.push(newTag)
- value = _.uniq(value)
+
+ if (!_.includes(value, newTag)) {
+ value.push(newTag)
+ }
+
+ if (this.props.saveTagsAlphabetically) {
+ value = _.sortBy(value)
+ }
this.setState({
newTag: ''
@@ -179,19 +186,34 @@ class TagSelect extends React.Component {
}
render () {
- const { value, className } = this.props
+ const { value, className, showTagsAlphabetically, coloredTags } = this.props
const tagList = _.isArray(value)
- ? value.map((tag) => {
+ ? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
+ const wrapperStyle = {}
+ const textStyle = {}
+ const BLACK = '#333333'
+ const WHITE = '#f1f1f1'
+ const color = coloredTags[tag]
+ const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE })
+ let iconRemove = '../resources/icon/icon-x.svg'
+ if (color) {
+ wrapperStyle.backgroundColor = color
+ textStyle.color = invertedColor
+ }
+ if (invertedColor === WHITE) {
+ iconRemove = '../resources/icon/icon-x-light.svg'
+ }
return (
- this.handleTagLabelClick(tag)}>#{tag}
+ this.handleTagLabelClick(tag)}>#{tag}
this.handleTagRemoveButtonClick(tag)}
>
-
+
)
@@ -240,7 +262,8 @@ TagSelect.contextTypes = {
TagSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
- onChange: PropTypes.func
+ onChange: PropTypes.func,
+ coloredTags: PropTypes.object
}
export default CSSModules(TagSelect, styles)
diff --git a/browser/main/Detail/TagSelect.styl b/browser/main/Detail/TagSelect.styl
index c6b13f3c..844561c6 100644
--- a/browser/main/Detail/TagSelect.styl
+++ b/browser/main/Detail/TagSelect.styl
@@ -3,19 +3,18 @@
align-items center
user-select none
vertical-align middle
- width 100%
- overflow-x scroll
+ width 96%
+ overflow-x auto
white-space nowrap
- margin-top 31px
+ top 50px
position absolute
-
-.root::-webkit-scrollbar
- display none
+ &::-webkit-scrollbar
+ height 8px
.tag
display flex
align-items center
- margin 0px 2px
+ margin 0px 2px 2px
padding 2px 4px
background-color alpha($ui-tag-backgroundColor, 3%)
border-radius 4px
diff --git a/browser/main/Detail/ToggleModeButton.js b/browser/main/Detail/ToggleModeButton.js
index c414a3e5..fcbaab34 100644
--- a/browser/main/Detail/ToggleModeButton.js
+++ b/browser/main/Detail/ToggleModeButton.js
@@ -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
-}) => (
-
-
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/Detail/ToggleModeButton.styl b/browser/main/Detail/ToggleModeButton.styl
index 73f5acbd..2b47b932 100644
--- a/browser/main/Detail/ToggleModeButton.styl
+++ b/browser/main/Detail/ToggleModeButton.styl
@@ -40,6 +40,11 @@
opacity 0
transition 0.1s
+.tooltip:lang(ja)
+ @extend .tooltip
+ left -8px
+ width 70px
+
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
diff --git a/browser/main/Detail/TrashButton.js b/browser/main/Detail/TrashButton.js
index 473c2d0b..d26be66e 100644
--- a/browser/main/Detail/TrashButton.js
+++ b/browser/main/Detail/TrashButton.js
@@ -11,7 +11,7 @@ const TrashButton = ({
onClick={(e) => onClick(e)}
>
-
{i18n.__('Trash')}
+
{i18n.__('Trash')}
)
diff --git a/browser/main/Detail/TrashButton.styl b/browser/main/Detail/TrashButton.styl
index 7c7af878..a82cfa6b 100644
--- a/browser/main/Detail/TrashButton.styl
+++ b/browser/main/Detail/TrashButton.styl
@@ -17,6 +17,10 @@
opacity 0
transition 0.1s
+.tooltip:lang(ja)
+ @extend .tooltip
+ right 46px
+
.control-trashButton--in-trash
top 60px
topBarButtonRight()
diff --git a/browser/main/Main.js b/browser/main/Main.js
index 1ffb2f74..26fc8377 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,
@@ -97,12 +96,14 @@ class Main extends React.Component {
{
name: 'example.html',
mode: 'html',
- content: "\n\n
Enjoy Boostnote! \n\n"
+ content: "\n\n
Enjoy Boostnote! \n\n",
+ linesHighlighted: []
},
{
name: 'example.js',
mode: 'javascript',
- content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
+ content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)",
+ linesHighlighted: []
}
]
})
@@ -168,11 +169,24 @@ class Main extends React.Component {
}
})
+ delete CodeMirror.keyMap.emacs['Ctrl-V']
+
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
+ eventEmitter.on('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
}
componentWillUnmount () {
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
+ eventEmitter.off('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
+ }
+
+ toggleMenuBarVisible () {
+ const { config } = this.props
+ const { ui } = config
+
+ const newUI = Object.assign(ui, {showMenuBar: !ui.showMenuBar})
+ const newConfig = Object.assign(config, newUI)
+ ConfigManager.set(newConfig)
}
handleLeftSlideMouseDown (e) {
@@ -233,8 +247,8 @@ class Main extends React.Component {
if (this.state.isRightSliderFocused) {
const offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
- if (newListWidth < 10) {
- newListWidth = 10
+ if (newListWidth < 180) {
+ newListWidth = 180
} else if (newListWidth > 600) {
newListWidth = 600
}
diff --git a/browser/main/NewNoteButton/index.js b/browser/main/NewNoteButton/index.js
index e739a550..c34443be 100644
--- a/browser/main/NewNoteButton/index.js
+++ b/browser/main/NewNoteButton/index.js
@@ -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
})
}
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index 30ad93c3..33a0adf0 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -2,7 +2,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
-import debounceRender from 'react-debounce-render'
import styles from './NoteList.styl'
import moment from 'moment'
import _ from 'lodash'
@@ -56,7 +55,6 @@ class NoteList extends React.Component {
super(props)
this.selectNextNoteHandler = () => {
- console.log('fired next')
this.selectNextNote()
}
this.selectPriorNoteHandler = () => {
@@ -65,13 +63,14 @@ class NoteList extends React.Component {
this.focusHandler = () => {
this.refs.list.focus()
}
- this.alertIfSnippetHandler = () => {
- this.alertIfSnippet()
+ this.alertIfSnippetHandler = (event, msg) => {
+ this.alertIfSnippet(msg)
}
this.importFromFileHandler = this.importFromFile.bind(this)
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
+ this.cloneNote = this.cloneNote.bind(this)
this.deleteNote = this.deleteNote.bind(this)
this.focusNote = this.focusNote.bind(this)
this.pinToTop = this.pinToTop.bind(this)
@@ -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: []
}
@@ -95,6 +96,7 @@ class NoteList extends React.Component {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ee.on('list:next', this.selectNextNoteHandler)
ee.on('list:prior', this.selectPriorNoteHandler)
+ ee.on('list:clone', this.cloneNote)
ee.on('list:focus', this.focusHandler)
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.on('import:file', this.importFromFileHandler)
@@ -117,6 +119,7 @@ class NoteList extends React.Component {
ee.off('list:next', this.selectNextNoteHandler)
ee.off('list:prior', this.selectPriorNoteHandler)
+ ee.off('list:clone', this.cloneNote)
ee.off('list:focus', this.focusHandler)
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.off('import:file', this.importFromFileHandler)
@@ -172,16 +175,15 @@ class NoteList extends React.Component {
}
}
- focusNote (selectedNoteKeys, noteKey) {
+ focusNote (selectedNoteKeys, noteKey, pathname) {
const { router } = this.context
- const { location } = this.props
this.setState({
selectedNoteKeys
})
router.push({
- pathname: location.pathname,
+ pathname,
query: {
key: noteKey
}
@@ -200,6 +202,7 @@ class NoteList extends React.Component {
}
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
+ const { location } = this.props
let targetIndex = this.getTargetIndex()
@@ -216,7 +219,7 @@ class NoteList extends React.Component {
selectedNoteKeys.push(priorNoteKey)
}
- this.focusNote(selectedNoteKeys, priorNoteKey)
+ this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
ee.emit('list:moved')
}
@@ -227,6 +230,7 @@ class NoteList extends React.Component {
}
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
+ const { location } = this.props
let targetIndex = this.getTargetIndex()
const isTargetLastNote = targetIndex === this.notes.length - 1
@@ -249,7 +253,7 @@ class NoteList extends React.Component {
selectedNoteKeys.push(nextNoteKey)
}
- this.focusNote(selectedNoteKeys, nextNoteKey)
+ this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
ee.emit('list:moved')
}
@@ -261,13 +265,13 @@ class NoteList extends React.Component {
}
const selectedNoteKeys = [noteHash]
- this.focusNote(selectedNoteKeys, noteHash)
+ this.focusNote(selectedNoteKeys, noteHash, '/home')
ee.emit('list:moved')
}
handleNoteListKeyDown (e) {
- if (e.metaKey || e.ctrlKey) return true
+ if (e.metaKey) return true
// A key
if (e.keyCode === 65 && !e.shiftKey) {
@@ -275,12 +279,6 @@ class NoteList extends React.Component {
ee.emit('top:new-note')
}
- // D key
- if (e.keyCode === 68) {
- e.preventDefault()
- this.deleteNote()
- }
-
// E key
if (e.keyCode === 69) {
e.preventDefault()
@@ -307,6 +305,8 @@ class NoteList extends React.Component {
if (e.shiftKey) {
this.setState({ shiftKeyDown: true })
+ } else if (e.ctrlKey) {
+ this.setState({ ctrlKeyDown: true })
}
}
@@ -314,6 +314,10 @@ class NoteList extends React.Component {
if (!e.shiftKey) {
this.setState({ shiftKeyDown: false })
}
+
+ if (!e.ctrlKey) {
+ this.setState({ ctrlKeyDown: false })
+ }
}
getNotes () {
@@ -390,25 +394,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({
@@ -447,14 +491,21 @@ class NoteList extends React.Component {
})
}
- alertIfSnippet () {
+ alertIfSnippet (msg) {
+ const warningMessage = (msg) => ({
+ 'export-txt': 'Text export',
+ 'export-md': 'Markdown export',
+ 'export-html': 'HTML export',
+ 'print': 'Print'
+ })[msg]
+
const targetIndex = this.getTargetIndex()
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Sorry!'),
- detail: i18n.__('md/text import is available only a markdown note.'),
- buttons: [i18n.__('OK'), i18n.__('Cancel')]
+ detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
+ buttons: [i18n.__('OK')]
})
}
}
@@ -605,18 +656,21 @@ class NoteList extends React.Component {
})
)
.then((data) => {
- data.forEach((item) => {
- dispatch({
- type: 'DELETE_NOTE',
- storageKey: item.storageKey,
- noteKey: item.noteKey
+ const dispatchHandler = () => {
+ data.forEach((item) => {
+ dispatch({
+ type: 'DELETE_NOTE',
+ storageKey: item.storageKey,
+ noteKey: item.noteKey
+ })
})
- })
+ }
+ ee.once('list:next', dispatchHandler)
})
+ .then(() => ee.emit('list:next'))
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
- console.log('Notes were all deleted')
} else {
if (!confirmDeleteNote(confirmDeletion, false)) return
@@ -636,8 +690,8 @@ class NoteList extends React.Component {
})
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
- console.log('Notes went to trash')
})
+ .then(() => ee.emit('list:next'))
.catch((err) => {
console.error('Notes could not go to trash: ' + err)
})
@@ -661,7 +715,8 @@ class NoteList extends React.Component {
type: firstNote.type,
folder: folder.key,
title: firstNote.title + ' ' + i18n.__('copy'),
- content: firstNote.content
+ content: firstNote.content,
+ linesHighlighted: firstNote.linesHighlighted
})
.then((note) => {
attachmentManagement.cloneAttachments(firstNote, note)
@@ -996,6 +1051,8 @@ class NoteList extends React.Component {
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
+ showTagsAlphabetically={config.ui.showTagsAlphabetically}
+ coloredTags={config.coloredTags}
/>
)
}
@@ -1078,4 +1135,4 @@ NoteList.propTypes = {
})
}
-export default debounceRender(CSSModules(NoteList, styles))
+export default CSSModules(NoteList, styles)
diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js
index d17314b3..e336f3ce 100644
--- a/browser/main/SideNav/StorageItem.js
+++ b/browser/main/SideNav/StorageItem.js
@@ -25,7 +25,8 @@ class StorageItem extends React.Component {
const { storage } = this.props
this.state = {
- isOpen: !!storage.isOpen
+ isOpen: !!storage.isOpen,
+ draggedOver: null
}
}
@@ -204,6 +205,20 @@ class StorageItem extends React.Component {
folderKey: data.folderKey,
fileType: data.fileType
})
+ return data
+ })
+ .then(data => {
+ dialog.showMessageBox(remote.getCurrentWindow(), {
+ type: 'info',
+ message: 'Exported to "' + data.exportDir + '"'
+ })
+ })
+ .catch(err => {
+ dialog.showErrorBox(
+ 'Export error',
+ err ? err.message || err : 'Unexpected error during export'
+ )
+ throw err
})
}
})
@@ -231,14 +246,20 @@ class StorageItem extends React.Component {
}
}
- handleDragEnter (e) {
- e.dataTransfer.setData('defaultColor', e.target.style.backgroundColor)
- e.target.style.backgroundColor = 'rgba(129, 130, 131, 0.08)'
+ handleDragEnter (e, key) {
+ e.preventDefault()
+ if (this.state.draggedOver === key) { return }
+ this.setState({
+ draggedOver: key
+ })
}
handleDragLeave (e) {
- e.target.style.opacity = '1'
- e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
+ e.preventDefault()
+ if (this.state.draggedOver === null) { return }
+ this.setState({
+ draggedOver: null
+ })
}
dropNote (storage, folder, dispatch, location, noteData) {
@@ -263,8 +284,12 @@ class StorageItem extends React.Component {
}
handleDrop (e, storage, folder, dispatch, location) {
- e.target.style.opacity = '1'
- e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
+ e.preventDefault()
+ if (this.state.draggedOver !== null) {
+ this.setState({
+ draggedOver: null
+ })
+ }
const noteData = JSON.parse(e.dataTransfer.getData('note'))
this.dropNote(storage, folder, dispatch, location, noteData)
}
@@ -274,7 +299,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)
@@ -291,16 +316,22 @@ class StorageItem extends React.Component {
this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
folderName={folder.name}
folderColor={folder.color}
isFolded={isFolded}
noteCount={noteCount}
- handleDrop={(e) => this.handleDrop(e, storage, folder, dispatch, location)}
- handleDragEnter={this.handleDragEnter}
- handleDragLeave={this.handleDragLeave}
+ handleDrop={(e) => {
+ this.handleDrop(e, storage, folder, dispatch, location)
+ }}
+ handleDragEnter={(e) => {
+ this.handleDragEnter(e, folder.key)
+ }}
+ handleDragLeave={(e) => {
+ this.handleDragLeave(e, folder)
+ }}
/>
)
})
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index e78c9c77..640bedbf 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -19,9 +19,31 @@ 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'
+import ColorPicker from 'browser/components/ColorPicker'
+
+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
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ colorPicker: {
+ show: false,
+ color: null,
+ tagName: null,
+ targetRect: null
+ }
+ }
+
+ this.dismissColorPicker = this.dismissColorPicker.bind(this)
+ this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this)
+ this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
+ }
componentDidMount () {
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
@@ -99,9 +121,64 @@ class SideNav extends React.Component {
click: this.deleteTag.bind(this, tag)
})
+ menu.push({
+ label: i18n.__('Customize Color'),
+ click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect())
+ })
+
context.popup(menu)
}
+ dismissColorPicker () {
+ this.setState({
+ colorPicker: {
+ show: false
+ }
+ })
+ }
+
+ displayColorPicker (tagName, rect) {
+ const { config } = this.props
+ this.setState({
+ colorPicker: {
+ show: true,
+ color: config.coloredTags[tagName],
+ tagName,
+ targetRect: rect
+ }
+ })
+ }
+
+ handleColorPickerConfirm (color) {
+ const { dispatch, config: {coloredTags} } = this.props
+ const { colorPicker: { tagName } } = this.state
+ const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex})
+
+ const config = { coloredTags: newColoredTags }
+ ConfigManager.set(config)
+ dispatch({
+ type: 'SET_CONFIG',
+ config
+ })
+ this.dismissColorPicker()
+ }
+
+ handleColorPickerReset () {
+ const { dispatch, config: {coloredTags} } = this.props
+ const { colorPicker: { tagName } } = this.state
+ const newColoredTags = Object.assign({}, coloredTags)
+
+ delete newColoredTags[tagName]
+
+ const config = { coloredTags: newColoredTags }
+ ConfigManager.set(config)
+ dispatch({
+ type: 'SET_CONFIG',
+ config
+ })
+ this.dismissColorPicker()
+ }
+
handleToggleButtonClick (e) {
const { dispatch, config } = this.props
@@ -202,12 +279,21 @@ class SideNav extends React.Component {
tagListComponent () {
const { data, location, config } = this.props
- const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
+ const { colorPicker } = this.state
+ 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))
}
@@ -224,10 +310,11 @@ class SideNav extends React.Component {
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
handleContextMenu={this.handleTagContextMenu.bind(this)}
- isActive={this.getTagActive(location.pathname, tag.name)}
+ isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)}
isRelated={tag.related}
key={tag.name}
count={tag.size}
+ color={config.coloredTags[tag.name]}
/>
)
})
@@ -257,7 +344,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) {
@@ -289,7 +376,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) {
@@ -297,6 +384,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 }) => {
@@ -306,7 +395,6 @@ class SideNav extends React.Component {
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
- console.log('Trash emptied')
}
handleFilterButtonContextMenu (event) {
@@ -319,6 +407,7 @@ class SideNav extends React.Component {
render () {
const { data, location, config, dispatch } = this.props
+ const { colorPicker: colorPickerState } = this.state
const isFolded = config.isSideNavFolded
@@ -335,6 +424,20 @@ class SideNav extends React.Component {
useDragHandle
/>
})
+
+ let colorPicker
+ if (colorPickerState.show) {
+ colorPicker = (
+
+ )
+ }
+
const style = {}
if (!isFolded) style.width = this.props.width
const isTagActive = location.pathname.match(/tag/)
@@ -354,6 +457,7 @@ class SideNav extends React.Component {