mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-24 23:23:09 +00:00
Merge branch 'master' into tags
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
24
browser/lib/markdown-it-frontmatter.js
Normal file
24
browser/lib/markdown-it-frontmatter.js
Normal file
@@ -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' ]
|
||||
})
|
||||
}
|
||||
100
browser/lib/markdown-toc-generator.js
Normal file
100
browser/lib/markdown-toc-generator.js
Normal file
@@ -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 = '<!-- toc -->'
|
||||
const TOC_MARKER_END = '<!-- tocstop -->'
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -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'), '', {
|
||||
|
||||
62
browser/lib/newNote.js
Normal file
62
browser/lib/newNote.js
Normal file
@@ -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')
|
||||
})
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className={_.isString(className)
|
||||
? 'TagSelect ' + className
|
||||
@@ -152,13 +203,25 @@ class TagSelect extends React.Component {
|
||||
styleName='root'
|
||||
>
|
||||
{tagList}
|
||||
<input styleName='newTag'
|
||||
<Autosuggest
|
||||
ref='newTag'
|
||||
value={this.state.newTag}
|
||||
placeholder={i18n.__('Add tag...')}
|
||||
onChange={(e) => 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 => (
|
||||
<div>
|
||||
{suggestion.name}
|
||||
</div>
|
||||
)}
|
||||
inputProps={{
|
||||
placeholder: i18n.__('Add tag...'),
|
||||
value: newTag,
|
||||
onChange: this.onInputChange,
|
||||
onKeyDown: this.onInputKeyDown,
|
||||
onBlur: this.onInputBlur
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -169,7 +232,6 @@ TagSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func
|
||||
|
||||
}
|
||||
|
||||
export default CSSModules(TagSelect, styles)
|
||||
|
||||
@@ -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
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
63
browser/main/lib/dataApi/exportStorage.js
Normal file
63
browser/main/lib/dataApi/exportStorage.js
Normal file
@@ -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
|
||||
@@ -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'),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Default New Note')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.ui.defaultNote}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
ref='defaultNote'
|
||||
>
|
||||
<option value='ALWAYS_ASK'>{i18n.__('Always Ask')}</option>
|
||||
<option value='MARKDOWN_NOTE'>{i18n.__('Markdown Note')}</option>
|
||||
<option value='SNIPPET_NOTE'>{i18n.__('Snippet Note')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
|
||||
@@ -216,16 +216,10 @@ function data (state = defaultDataMap(), action) {
|
||||
return state
|
||||
}
|
||||
case 'UPDATE_FOLDER':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
return state
|
||||
case 'REORDER_FOLDER':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
return state
|
||||
case 'EXPORT_FOLDER':
|
||||
case 'RENAME_STORAGE':
|
||||
case 'EXPORT_STORAGE':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
@@ -355,11 +349,6 @@ function data (state = defaultDataMap(), action) {
|
||||
})
|
||||
}
|
||||
return state
|
||||
case 'RENAME_STORAGE':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
return state
|
||||
case 'EXPAND_STORAGE':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
|
||||
109
browser/styles/Detail/TagSelect.styl
Normal file
109
browser/styles/Detail/TagSelect.styl
Normal file
@@ -0,0 +1,109 @@
|
||||
.TagSelect
|
||||
.react-autosuggest__input
|
||||
box-sizing border-box
|
||||
border none
|
||||
background-color transparent
|
||||
outline none
|
||||
padding 0 4px
|
||||
font-size 13px
|
||||
|
||||
ul
|
||||
position fixed
|
||||
z-index 999
|
||||
box-sizing border-box
|
||||
list-style none
|
||||
padding 0
|
||||
margin 0
|
||||
|
||||
border-radius 4px
|
||||
margin .2em 0 0
|
||||
background-color $ui-noteList-backgroundColor
|
||||
border 1px solid rgba(0,0,0,.3)
|
||||
box-shadow .05em .2em .6em rgba(0,0,0,.2)
|
||||
text-shadow none
|
||||
|
||||
&:empty,
|
||||
&[hidden]
|
||||
display none
|
||||
|
||||
&:before
|
||||
content ""
|
||||
position absolute
|
||||
top -7px
|
||||
left 14px
|
||||
width 0 height 0
|
||||
padding 6px
|
||||
background-color $ui-noteList-backgroundColor
|
||||
border inherit
|
||||
border-right 0
|
||||
border-bottom 0
|
||||
-webkit-transform rotate(45deg)
|
||||
transform rotate(45deg)
|
||||
|
||||
li
|
||||
position relative
|
||||
padding 6px 18px 6px 10px
|
||||
cursor pointer
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color alpha($ui-button--active-backgroundColor, 40%)
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.TagSelect
|
||||
.react-autosuggest__input
|
||||
color $ui-dark-text-color
|
||||
|
||||
ul
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
&:before
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.TagSelect
|
||||
.react-autosuggest__input
|
||||
color $ui-monokai-text-color
|
||||
|
||||
ul
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
&:before
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.TagSelect
|
||||
.react-autosuggest__input
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
ul
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
&:before
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="white"]
|
||||
.TagSelect
|
||||
ul
|
||||
background-color $ui-white-noteList-backgroundColor
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color $ui-button--active-backgroundColor
|
||||
Reference in New Issue
Block a user