diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index 7f6b4bbd..304171ba 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -604,7 +604,10 @@ export default class CodeEditor extends React.Component {
body,
'text/html'
)
- const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
+ const escapePipe = (str) => {
+ return str.replace('|', '\\|')
+ }
+ const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})`
resolve(linkWithTitle)
} catch (e) {
reject(e)
diff --git a/browser/lib/findNoteTitle.js b/browser/lib/findNoteTitle.js
index 81c9400f..b954f172 100644
--- a/browser/lib/findNoteTitle.js
+++ b/browser/lib/findNoteTitle.js
@@ -3,6 +3,17 @@ export function findNoteTitle (value) {
let title = null
let isInsideCodeBlock = false
+ if (splitted[0] === '---') {
+ let line = 0
+ while (++line < splitted.length) {
+ if (splitted[line] === '---') {
+ splitted.splice(0, line + 1)
+
+ break
+ }
+ }
+ }
+
splitted.some((line, index) => {
const trimmedLine = line.trim()
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
diff --git a/browser/lib/markdown-it-frontmatter.js b/browser/lib/markdown-it-frontmatter.js
new file mode 100644
index 00000000..66d8ce89
--- /dev/null
+++ b/browser/lib/markdown-it-frontmatter.js
@@ -0,0 +1,24 @@
+'use strict'
+
+module.exports = function frontMatterPlugin (md) {
+ function frontmatter (state, startLine, endLine, silent) {
+ if (startLine !== 0 || state.src.substr(startLine, state.eMarks[0]) !== '---') {
+ return false
+ }
+
+ let line = 0
+ while (++line < state.lineMax) {
+ if (state.src.substring(state.bMarks[line], state.eMarks[line]) === '---') {
+ state.line = line + 1
+
+ return true
+ }
+ }
+
+ return false
+ }
+
+ md.block.ruler.before('table', 'frontmatter', frontmatter, {
+ alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
+ })
+}
diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js
new file mode 100644
index 00000000..716be83a
--- /dev/null
+++ b/browser/lib/markdown-toc-generator.js
@@ -0,0 +1,100 @@
+/**
+ * @fileoverview Markdown table of contents generator
+ */
+
+import toc from 'markdown-toc'
+import diacritics from 'diacritics-map'
+import stripColor from 'strip-color'
+
+const EOL = require('os').EOL
+
+/**
+ * @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
+ */
+function caseSensitiveSlugify (str) {
+ function replaceDiacritics (str) {
+ return str.replace(/[À-ž]/g, function (ch) {
+ return diacritics[ch] || ch
+ })
+ }
+
+ 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
+}
+
+const TOC_MARKER_START = ''
+const TOC_MARKER_END = ''
+
+/**
+ * Takes care of proper updating given editor with TOC.
+ * If TOC doesn't exit in the editor, it's inserted at current caret position.
+ * Otherwise,TOC is updated in place.
+ * @param editor CodeMirror editor to be updated with TOC
+ */
+export function generateInEditor (editor) {
+ const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
+
+ function tocExistsInEditor () {
+ return tocRegex.test(editor.getValue())
+ }
+
+ function updateExistingToc () {
+ const toc = generate(editor.getValue())
+ const search = editor.getSearchCursor(tocRegex)
+ while (search.findNext()) {
+ search.replace(toc)
+ }
+ }
+
+ function addTocAtCursorPosition () {
+ const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity}))
+ editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
+ }
+
+ if (tocExistsInEditor()) {
+ updateExistingToc()
+ } else {
+ addTocAtCursorPosition()
+ }
+}
+
+/**
+ * Generates MD TOC based on MD document passed as string.
+ * @param markdownText MD document
+ * @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
+}
+
+function wrapTocWithEol (toc, editor) {
+ const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
+ const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL
+ return leftWrap + toc + rightWrap
+}
+
+export default {
+ generate,
+ generateInEditor
+}
diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js
index 49fd2f86..ba57ec6b 100644
--- a/browser/lib/markdown.js
+++ b/browser/lib/markdown.js
@@ -149,6 +149,7 @@ class Markdown {
})
this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'))
+ this.md.use(require('./markdown-it-frontmatter'))
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
diff --git a/browser/lib/newNote.js b/browser/lib/newNote.js
new file mode 100644
index 00000000..bed69735
--- /dev/null
+++ b/browser/lib/newNote.js
@@ -0,0 +1,62 @@
+import { hashHistory } from 'react-router'
+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) {
+ AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
+ AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
+ return dataApi
+ .createNote(storage, {
+ type: 'MARKDOWN_NOTE',
+ folder: folder,
+ title: '',
+ content: ''
+ })
+ .then(note => {
+ const noteHash = note.key
+ dispatch({
+ type: 'UPDATE_NOTE',
+ note: note
+ })
+
+ hashHistory.push({
+ pathname: location.pathname,
+ query: { key: noteHash }
+ })
+ ee.emit('list:jump', noteHash)
+ ee.emit('detail:focus')
+ })
+}
+
+export function createSnippetNote (storage, folder, dispatch, location, config) {
+ AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
+ AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
+ return dataApi
+ .createNote(storage, {
+ type: 'SNIPPET_NOTE',
+ folder: folder,
+ title: '',
+ description: '',
+ snippets: [
+ {
+ name: '',
+ mode: config.editor.snippetDefaultLanguage || 'text',
+ content: ''
+ }
+ ]
+ })
+ .then(note => {
+ const noteHash = note.key
+ dispatch({
+ type: 'UPDATE_NOTE',
+ note: note
+ })
+ hashHistory.push({
+ pathname: location.pathname,
+ query: { key: noteHash }
+ })
+ ee.emit('list:jump', noteHash)
+ ee.emit('detail:focus')
+ })
+}
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index 1e30d28b..4e6d057a 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -29,6 +29,7 @@ import { formatDate } from 'browser/lib/date-formatter'
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
+import markdownToc from 'browser/lib/markdown-toc-generator'
class MarkdownNoteDetail extends React.Component {
constructor (props) {
@@ -47,6 +48,7 @@ class MarkdownNoteDetail extends React.Component {
this.dispatchTimer = null
this.toggleLockButton = this.handleToggleLockButton.bind(this)
+ this.generateToc = () => this.handleGenerateToc()
}
focus () {
@@ -59,6 +61,7 @@ class MarkdownNoteDetail extends React.Component {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType)
})
+ ee.on('code:generate-toc', this.generateToc)
}
componentWillReceiveProps (nextProps) {
@@ -75,6 +78,7 @@ class MarkdownNoteDetail extends React.Component {
componentWillUnmount () {
ee.off('topbar:togglelockbutton', this.toggleLockButton)
+ ee.off('code:generate-toc', this.generateToc)
if (this.saveQueue != null) this.saveNow()
}
@@ -262,6 +266,11 @@ class MarkdownNoteDetail extends React.Component {
}
}
+ handleGenerateToc () {
+ const editor = this.refs.content.refs.code.editor
+ markdownToc.generateInEditor(editor)
+ }
+
handleFocus (e) {
this.focus()
}
@@ -365,6 +374,7 @@ class MarkdownNoteDetail extends React.Component {
value={this.state.note.tags}
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
+ data={data}
onChange={this.handleUpdateTag.bind(this)}
/>
diff --git a/browser/main/Detail/NoteDetailInfo.styl b/browser/main/Detail/NoteDetailInfo.styl
index 8d454203..7166a497 100644
--- a/browser/main/Detail/NoteDetailInfo.styl
+++ b/browser/main/Detail/NoteDetailInfo.styl
@@ -13,6 +13,7 @@ $info-margin-under-border = 30px
display flex
align-items center
padding 0 20px
+ z-index 99
.info-left
padding 0 10px
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index 39d66eba..5bd45861 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -29,6 +29,7 @@ import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter'
import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
+import markdownToc from 'browser/lib/markdown-toc-generator'
const electron = require('electron')
const { remote } = electron
@@ -52,6 +53,7 @@ class SnippetNoteDetail extends React.Component {
}
this.scrollToNextTabThreshold = 0.7
+ this.generateToc = () => this.handleGenerateToc()
}
componentDidMount () {
@@ -65,6 +67,7 @@ class SnippetNoteDetail extends React.Component {
enableLeftArrow: allTabs.offsetLeft !== 0
})
}
+ ee.on('code:generate-toc', this.generateToc)
}
componentWillReceiveProps (nextProps) {
@@ -91,6 +94,16 @@ class SnippetNoteDetail extends React.Component {
componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
+ ee.off('code:generate-toc', this.generateToc)
+ }
+
+ handleGenerateToc () {
+ const { note, snippetIndex } = this.state
+ const currentMode = note.snippets[snippetIndex].mode
+ if (currentMode.includes('Markdown')) {
+ const currentEditor = this.refs[`code-${snippetIndex}`].refs.code.editor
+ markdownToc.generateInEditor(currentEditor)
+ }
}
handleChange (e) {
@@ -441,7 +454,7 @@ class SnippetNoteDetail extends React.Component {
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
- if (isSuper && !e.shiftKey) {
+ if (isSuper && !e.shiftKey && !e.altKey) {
e.preventDefault()
this.addSnippet()
}
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js
index ecaebf2e..ffa98786 100644
--- a/browser/main/Detail/TagSelect.js
+++ b/browser/main/Detail/TagSelect.js
@@ -6,71 +6,33 @@ import _ from 'lodash'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter'
+import Autosuggest from 'react-autosuggest'
class TagSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
- newTag: ''
+ newTag: '',
+ suggestions: []
}
- this.addtagHandler = this.handleAddTag.bind(this)
+
+ this.handleAddTag = this.handleAddTag.bind(this)
+ this.onInputBlur = this.onInputBlur.bind(this)
+ this.onInputChange = this.onInputChange.bind(this)
+ this.onInputKeyDown = this.onInputKeyDown.bind(this)
+ this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
+ this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
+ this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
}
- componentDidMount () {
- this.value = this.props.value
- ee.on('editor:add-tag', this.addtagHandler)
- }
-
- componentDidUpdate () {
- this.value = this.props.value
- }
-
- componentWillUnmount () {
- ee.off('editor:add-tag', this.addtagHandler)
- }
-
- handleAddTag () {
- this.refs.newTag.focus()
- }
-
- handleNewTagInputKeyDown (e) {
- switch (e.keyCode) {
- case 9:
- e.preventDefault()
- this.submitTag()
- break
- case 13:
- this.submitTag()
- break
- case 8:
- if (this.refs.newTag.value.length === 0) {
- this.removeLastTag()
- }
- }
- }
-
- handleNewTagBlur (e) {
- this.submitTag()
- }
-
- removeLastTag () {
- this.removeTagByCallback((value) => {
- value.pop()
- })
- }
-
- reset () {
- this.setState({
- newTag: ''
- })
- }
-
- submitTag () {
+ addNewTag (newTag) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
- let { value } = this.props
- let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
- newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
+
+ newTag = newTag.trim().replace(/ +/g, '_')
+ if (newTag.charAt(0) === '#') {
+ newTag.substring(1)
+ }
if (newTag.length <= 0) {
this.setState({
@@ -79,17 +41,12 @@ class TagSelect extends React.Component {
return
}
+ let { value } = this.props
value = _.isArray(value)
? value.slice()
: []
-
- if (!_.includes(value, newTag)) {
- value.push(newTag)
- }
-
- if (this.props.saveTagsAlphabetically) {
- value = _.sortBy(value)
- }
+ value.push(newTag)
+ value = _.uniq(value)
this.setState({
newTag: ''
@@ -99,10 +56,36 @@ class TagSelect extends React.Component {
})
}
- handleNewTagInputChange (e) {
- this.setState({
- newTag: this.refs.newTag.value
- })
+ buildSuggestions () {
+ this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
+ (tag, name) => ({
+ name,
+ nameLC: name.toLowerCase(),
+ size: tag.size
+ })
+ ).filter(
+ tag => tag.size > 0
+ ), ['name'])
+ }
+
+ componentDidMount () {
+ this.value = this.props.value
+
+ this.buildSuggestions()
+
+ ee.on('editor:add-tag', this.handleAddTag)
+ }
+
+ componentDidUpdate () {
+ this.value = this.props.value
+ }
+
+ componentWillUnmount () {
+ ee.off('editor:add-tag', this.handleAddTag)
+ }
+
+ handleAddTag () {
+ this.refs.newTag.input.focus()
}
handleTagRemoveButtonClick (tag) {
@@ -111,6 +94,60 @@ class TagSelect extends React.Component {
}, tag)
}
+ onInputBlur (e) {
+ this.submitNewTag()
+ }
+
+ onInputChange (e, { newValue, method }) {
+ this.setState({
+ newTag: newValue
+ })
+ }
+
+ onInputKeyDown (e) {
+ switch (e.keyCode) {
+ case 9:
+ e.preventDefault()
+ this.submitNewTag()
+ break
+ case 13:
+ this.submitNewTag()
+ break
+ case 8:
+ if (this.state.newTag.length === 0) {
+ this.removeLastTag()
+ }
+ }
+ }
+
+ onSuggestionsClearRequested () {
+ this.setState({
+ suggestions: []
+ })
+ }
+
+ onSuggestionsFetchRequested ({ value }) {
+ const valueLC = value.toLowerCase()
+ const suggestions = _.filter(
+ this.suggestions,
+ tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
+ )
+
+ this.setState({
+ suggestions
+ })
+ }
+
+ onSuggestionSelected (event, { suggestion, suggestionValue }) {
+ this.addNewTag(suggestionValue)
+ }
+
+ removeLastTag () {
+ this.removeTagByCallback((value) => {
+ value.pop()
+ })
+ }
+
removeTagByCallback (callback, tag = null) {
let { value } = this.props
@@ -124,6 +161,18 @@ class TagSelect extends React.Component {
this.props.onChange()
}
+ reset () {
+ this.buildSuggestions()
+
+ this.setState({
+ newTag: ''
+ })
+ }
+
+ submitNewTag () {
+ this.addNewTag(this.refs.newTag.input.value)
+ }
+
render () {
const { value, className, showTagsAlphabetically } = this.props
@@ -144,6 +193,8 @@ class TagSelect extends React.Component {
})
: []
+ const { newTag, suggestions } = this.state
+
return (
{tagList}
-
this.handleNewTagInputChange(e)}
- onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
- onBlur={(e) => this.handleNewTagBlur(e)}
+ suggestions={suggestions}
+ onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
+ onSuggestionsClearRequested={this.onSuggestionsClearRequested}
+ onSuggestionSelected={this.onSuggestionSelected}
+ getSuggestionValue={suggestion => suggestion.name}
+ renderSuggestion={suggestion => (
+
+ {suggestion.name}
+
+ )}
+ inputProps={{
+ placeholder: i18n.__('Add tag...'),
+ value: newTag,
+ onChange: this.onInputChange,
+ onKeyDown: this.onInputKeyDown,
+ onBlur: this.onInputBlur
+ }}
/>
)
@@ -169,7 +232,6 @@ TagSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func
-
}
export default CSSModules(TagSelect, styles)
diff --git a/browser/main/Detail/TagSelect.styl b/browser/main/Detail/TagSelect.styl
index 0ff4c6a3..7dc9dfe4 100644
--- a/browser/main/Detail/TagSelect.styl
+++ b/browser/main/Detail/TagSelect.styl
@@ -42,14 +42,6 @@
color: $ui-text-color
padding 4px 16px 4px 8px
-.newTag
- box-sizing border-box
- border none
- background-color transparent
- outline none
- padding 0 4px
- font-size 13px
-
body[data-theme="dark"]
.tag
background-color alpha($ui-dark-tag-backgroundColor, 60%)
@@ -62,11 +54,6 @@ body[data-theme="dark"]
.tag-label
color $ui-dark-text-color
- .newTag
- border-color none
- background-color transparent
- color $ui-dark-text-color
-
body[data-theme="solarized-dark"]
.tag
background-color $ui-solarized-dark-tag-backgroundColor
@@ -78,11 +65,6 @@ body[data-theme="solarized-dark"]
.tag-label
color $ui-solarized-dark-text-color
- .newTag
- border-color none
- background-color transparent
- color $ui-solarized-dark-text-color
-
body[data-theme="monokai"]
.tag
background-color $ui-monokai-button-backgroundColor
@@ -92,9 +74,4 @@ body[data-theme="monokai"]
background-color transparent
.tag-label
- color $ui-monokai-text-color
-
- .newTag
- border-color none
- background-color transparent
- color $ui-monokai-text-color
+ color $ui-monokai-text-color
\ No newline at end of file
diff --git a/browser/main/NewNoteButton/index.js b/browser/main/NewNoteButton/index.js
index 85dc7f40..e739a550 100644
--- a/browser/main/NewNoteButton/index.js
+++ b/browser/main/NewNoteButton/index.js
@@ -7,6 +7,7 @@ import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import eventEmitter from 'browser/main/lib/eventEmitter'
import i18n from 'browser/lib/i18n'
+import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
const { remote } = require('electron')
const { dialog } = remote
@@ -37,13 +38,19 @@ class NewNoteButton extends React.Component {
const { location, dispatch, config } = this.props
const { storage, folder } = this.resolveTargetFolder()
- modal.open(NewNoteModal, {
- storage: storage.key,
- folder: folder.key,
- dispatch,
- location,
- config
- })
+ if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
+ createMarkdownNote(storage.key, folder.key, dispatch, location)
+ } else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
+ createSnippetNote(storage.key, folder.key, dispatch, location, config)
+ } else {
+ modal.open(NewNoteModal, {
+ storage: storage.key,
+ folder: folder.key,
+ dispatch,
+ location,
+ config
+ })
+ }
}
resolveTargetFolder () {
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index 44da782b..1c8fe69a 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -80,6 +80,7 @@ class NoteList extends React.Component {
this.getViewType = this.getViewType.bind(this)
this.restoreNote = this.restoreNote.bind(this)
this.copyNoteLink = this.copyNoteLink.bind(this)
+ this.navigate = this.navigate.bind(this)
// TODO: not Selected noteKeys but SelectedNote(for reusing)
this.state = {
@@ -98,6 +99,7 @@ class NoteList extends React.Component {
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.on('import:file', this.importFromFileHandler)
ee.on('list:jump', this.jumpNoteByHash)
+ ee.on('list:navigate', this.navigate)
}
componentWillReceiveProps (nextProps) {
@@ -687,6 +689,16 @@ class NoteList extends React.Component {
return copy(noteLink)
}
+ navigate (sender, pathname) {
+ const { router } = this.context
+ router.push({
+ pathname,
+ query: {
+ // key: noteKey
+ }
+ })
+ }
+
save (note) {
const { dispatch } = this.props
dataApi
diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js
index d72f0a8f..d17314b3 100644
--- a/browser/main/SideNav/StorageItem.js
+++ b/browser/main/SideNav/StorageItem.js
@@ -38,6 +38,22 @@ class StorageItem extends React.Component {
{
type: 'separator'
},
+ {
+ label: i18n.__('Export Storage'),
+ submenu: [
+ {
+ label: i18n.__('Export as txt'),
+ click: (e) => this.handleExportStorageClick(e, 'txt')
+ },
+ {
+ label: i18n.__('Export as md'),
+ click: (e) => this.handleExportStorageClick(e, 'md')
+ }
+ ]
+ },
+ {
+ type: 'separator'
+ },
{
label: i18n.__('Unlink Storage'),
click: (e) => this.handleUnlinkStorageClick(e)
@@ -68,6 +84,30 @@ class StorageItem extends React.Component {
}
}
+ handleExportStorageClick (e, fileType) {
+ const options = {
+ properties: ['openDirectory', 'createDirectory'],
+ buttonLabel: i18n.__('Select directory'),
+ title: i18n.__('Select a folder to export the files to'),
+ multiSelections: false
+ }
+ dialog.showOpenDialog(remote.getCurrentWindow(), options,
+ (paths) => {
+ if (paths && paths.length === 1) {
+ const { storage, dispatch } = this.props
+ dataApi
+ .exportStorage(storage.key, fileType, paths[0])
+ .then(data => {
+ dispatch({
+ type: 'EXPORT_STORAGE',
+ storage: data.storage,
+ fileType: data.fileType
+ })
+ })
+ }
+ })
+ }
+
handleToggleButtonClick (e) {
const { storage, dispatch } = this.props
const isOpen = !this.state.isOpen
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index 65f76691..86e148c8 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -210,12 +210,12 @@ class SideNav extends React.Component {
const tags = pathSegments[pathSegments.length - 1]
return (tags === 'alltags')
? []
- : tags.split(' ')
+ : tags.split(' ').map(tag => decodeURIComponent(tag))
}
handleClickTagListItem (name) {
const { router } = this.context
- router.push(`/tags/${name}`)
+ router.push(`/tags/${encodeURIComponent(name)}`)
}
handleSortTagsByChange (e) {
@@ -242,7 +242,7 @@ class SideNav extends React.Component {
} else {
listOfTags.push(tag)
}
- router.push(`/tags/${listOfTags.join(' ')}`)
+ router.push(`/tags/${listOfTags.map(tag => encodeURIComponent(tag)).join(' ')}`)
}
emptyTrash (entries) {
diff --git a/browser/main/global.styl b/browser/main/global.styl
index e4505a4e..815cff4e 100644
--- a/browser/main/global.styl
+++ b/browser/main/global.styl
@@ -132,6 +132,15 @@ body[data-theme="dark"]
.CodeMirror-foldgutter-folded:after
content: "\25B8"
+.CodeMirror-hover
+ padding 2px 4px 0 4px
+ position absolute
+ z-index 99
+
+.CodeMirror-hyperlink
+ cursor pointer
+
+
.sortableItemHelper
z-index modalZIndex + 5
@@ -156,3 +165,5 @@ body[data-theme="monokai"]
body[data-theme="default"]
.SideNav ::-webkit-scrollbar-thumb
background-color rgba(255, 255, 255, 0.3)
+
+@import '../styles/Detail/TagSelect.styl'
\ No newline at end of file
diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js
new file mode 100644
index 00000000..ce2c4573
--- /dev/null
+++ b/browser/main/lib/dataApi/exportStorage.js
@@ -0,0 +1,63 @@
+import { findStorage } from 'browser/lib/findStorage'
+import resolveStorageData from './resolveStorageData'
+import resolveStorageNotes from './resolveStorageNotes'
+import filenamify from 'filenamify'
+import * as path from 'path'
+import * as fs from 'fs'
+
+/**
+ * @param {String} storageKey
+ * @param {String} fileType
+ * @param {String} exportDir
+ *
+ * @return {Object}
+ * ```
+ * {
+ * storage: Object,
+ * fileType: String,
+ * exportDir: String
+ * }
+ * ```
+ */
+
+function exportStorage (storageKey, fileType, exportDir) {
+ let targetStorage
+ try {
+ targetStorage = findStorage(storageKey)
+ } catch (e) {
+ return Promise.reject(e)
+ }
+
+ return resolveStorageData(targetStorage)
+ .then(storage => (
+ resolveStorageNotes(storage).then(notes => ({storage, notes}))
+ ))
+ .then(function exportNotes (data) {
+ const { storage, notes } = data
+ const folderNamesMapping = {}
+ storage.folders.forEach(folder => {
+ const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
+ folderNamesMapping[folder.key] = folderExportedDir
+ // make sure directory exists
+ try {
+ fs.mkdirSync(folderExportedDir)
+ } catch (e) {}
+ })
+ notes
+ .filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
+ .forEach(markdownNote => {
+ const folderExportedDir = folderNamesMapping[markdownNote.folder]
+ const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}`
+ const notePath = path.join(folderExportedDir, snippetName)
+ fs.writeFileSync(notePath, markdownNote.content)
+ })
+
+ return {
+ storage,
+ fileType,
+ exportDir
+ }
+ })
+}
+
+module.exports = exportStorage
diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js
index 4e2f0061..92be6b93 100644
--- a/browser/main/lib/dataApi/index.js
+++ b/browser/main/lib/dataApi/index.js
@@ -9,6 +9,7 @@ const dataApi = {
deleteFolder: require('./deleteFolder'),
reorderFolder: require('./reorderFolder'),
exportFolder: require('./exportFolder'),
+ exportStorage: require('./exportStorage'),
createNote: require('./createNote'),
updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'),
diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js
index f6aa2c67..8b16f2a2 100644
--- a/browser/main/modals/NewNoteModal.js
+++ b/browser/main/modals/NewNoteModal.js
@@ -1,12 +1,9 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NewNoteModal.styl'
-import dataApi from 'browser/main/lib/dataApi'
-import { hashHistory } from 'react-router'
-import ee from 'browser/main/lib/eventEmitter'
import ModalEscButton from 'browser/components/ModalEscButton'
-import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
+import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
class NewNoteModal extends React.Component {
constructor (props) {
@@ -24,31 +21,10 @@ class NewNoteModal extends React.Component {
}
handleMarkdownNoteButtonClick (e) {
- AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
- AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
const { storage, folder, dispatch, location } = this.props
- dataApi
- .createNote(storage, {
- type: 'MARKDOWN_NOTE',
- folder: folder,
- title: '',
- content: ''
- })
- .then(note => {
- const noteHash = note.key
- dispatch({
- type: 'UPDATE_NOTE',
- note: note
- })
-
- hashHistory.push({
- pathname: location.pathname,
- query: { key: noteHash }
- })
- ee.emit('list:jump', noteHash)
- ee.emit('detail:focus')
- setTimeout(this.props.close, 200)
- })
+ createMarkdownNote(storage, folder, dispatch, location).then(() => {
+ setTimeout(this.props.close, 200)
+ })
}
handleMarkdownNoteButtonKeyDown (e) {
@@ -59,38 +35,10 @@ class NewNoteModal extends React.Component {
}
handleSnippetNoteButtonClick (e) {
- AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
- AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
const { storage, folder, dispatch, location, config } = this.props
-
- dataApi
- .createNote(storage, {
- type: 'SNIPPET_NOTE',
- folder: folder,
- title: '',
- description: '',
- snippets: [
- {
- name: '',
- mode: config.editor.snippetDefaultLanguage || 'text',
- content: ''
- }
- ]
- })
- .then(note => {
- const noteHash = note.key
- dispatch({
- type: 'UPDATE_NOTE',
- note: note
- })
- hashHistory.push({
- pathname: location.pathname,
- query: { key: noteHash }
- })
- ee.emit('list:jump', noteHash)
- ee.emit('detail:focus')
- setTimeout(this.props.close, 200)
- })
+ createSnippetNote(storage, folder, dispatch, location, config).then(() => {
+ setTimeout(this.props.close, 200)
+ })
}
handleSnippetNoteButtonKeyDown (e) {
diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js
index 2d564042..00a79958 100644
--- a/browser/main/modals/PreferencesModal/UiTab.js
+++ b/browser/main/modals/PreferencesModal/UiTab.js
@@ -67,6 +67,7 @@ class UiTab extends React.Component {
ui: {
theme: this.refs.uiTheme.value,
language: this.refs.uiLanguage.value,
+ defaultNote: this.refs.defaultNote.value,
showCopyNotification: this.refs.showCopyNotification.checked,
confirmDeletion: this.refs.confirmDeletion.checked,
showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
@@ -210,6 +211,22 @@ class UiTab extends React.Component {
+
+
+ {i18n.__('Default New Note')}
+
+
+
+
+
+