mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60df509fc6 | ||
|
|
ced237fbda | ||
|
|
8f90bb83e7 | ||
|
|
ba888a7165 | ||
|
|
9591a7cac2 | ||
|
|
e8fd53d8b7 | ||
|
|
3b7c36b1c9 | ||
|
|
2750a3ef30 | ||
|
|
7d59f51244 | ||
|
|
77542597f5 | ||
|
|
2a26e8a010 | ||
|
|
8b4c1a6c38 | ||
|
|
7d7e277cc6 | ||
|
|
cb1da609a4 | ||
|
|
e4fbdb35a3 | ||
|
|
c3586372e0 | ||
|
|
808d193543 | ||
|
|
0b65b70af2 | ||
|
|
deab34abd6 | ||
|
|
646cded650 | ||
|
|
9047320301 | ||
|
|
f16b054f01 | ||
|
|
fea856202d | ||
|
|
4f15cc3f08 | ||
|
|
3b524f6aba | ||
|
|
051ccad29a | ||
|
|
c82eba05d1 | ||
|
|
74af199afc | ||
|
|
c2afdba659 | ||
|
|
e6ae45f133 | ||
|
|
129ef6766b | ||
|
|
3194a7b1a2 | ||
|
|
46a733ba5b | ||
|
|
466844fc55 | ||
|
|
0cc793e3fe | ||
|
|
0fdfb385a4 | ||
|
|
419a4c6b2d | ||
|
|
55e9441547 | ||
|
|
7abff6ded4 | ||
|
|
a648d310a5 | ||
|
|
4b56fa56b5 | ||
|
|
18bf700936 | ||
|
|
67ddff736c | ||
|
|
1bba125a2a | ||
|
|
187acad592 | ||
|
|
5e07c7b3e1 | ||
|
|
cd942cf8b6 | ||
|
|
c43589fe6a | ||
|
|
d4594eff3b | ||
|
|
809a0e846b | ||
|
|
7f00ce2598 | ||
|
|
d7d77dbfe9 | ||
|
|
7a124c74cc | ||
|
|
f8549f4643 | ||
|
|
0476ff70da | ||
|
|
5ec541c3c1 | ||
|
|
7b920348f3 | ||
|
|
51b1ef41a1 | ||
|
|
12447effc9 | ||
|
|
dc1b059a9d | ||
|
|
a676ebf46c | ||
|
|
338f9fb5a9 | ||
|
|
98c9132d4a | ||
|
|
a054f12492 | ||
|
|
c42ac1df1b | ||
|
|
d9d46cda1c | ||
|
|
f678a17505 | ||
|
|
3da4bb69ce | ||
|
|
3e2b876c59 | ||
|
|
10daf2599d | ||
|
|
56d6eb7077 | ||
|
|
9b54272f8e | ||
|
|
685e003db8 | ||
|
|
701fc0bc80 | ||
|
|
e5518ac0fa | ||
|
|
8e1bf48cd1 | ||
|
|
8dd82e1a3b | ||
|
|
f04ad1e702 | ||
|
|
dc03bb76e6 | ||
|
|
d894bb4aab | ||
|
|
83da07a941 |
2
LICENSE
2
LICENSE
@@ -2,7 +2,7 @@ GPL-3.0
|
|||||||
|
|
||||||
Boostnote - an open source note-taking app made for programmers just like you.
|
Boostnote - an open source note-taking app made for programmers just like you.
|
||||||
|
|
||||||
Copyright (C) 2017 Maisin&Co., Inc.
|
Copyright (C) 2017 - 2018 BoostIO
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import path from 'path'
|
|||||||
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
|
||||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||||
|
|
||||||
@@ -47,6 +48,40 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.loadStyleHandler = (e) => {
|
this.loadStyleHandler = (e) => {
|
||||||
this.editor.refresh()
|
this.editor.refresh()
|
||||||
}
|
}
|
||||||
|
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||||
|
this.searchState = null
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearch (msg) {
|
||||||
|
const cm = this.editor
|
||||||
|
const component = this
|
||||||
|
|
||||||
|
if (component.searchState) cm.removeOverlay(component.searchState)
|
||||||
|
if (msg.length < 3) return
|
||||||
|
|
||||||
|
cm.operation(function () {
|
||||||
|
component.searchState = makeOverlay(msg, 'searching')
|
||||||
|
cm.addOverlay(component.searchState)
|
||||||
|
|
||||||
|
function makeOverlay (query, style) {
|
||||||
|
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi')
|
||||||
|
return {
|
||||||
|
token: function (stream) {
|
||||||
|
query.lastIndex = stream.pos
|
||||||
|
var match = query.exec(stream.string)
|
||||||
|
if (match && match.index === stream.pos) {
|
||||||
|
stream.pos += match[0].length || 1
|
||||||
|
return style
|
||||||
|
} else if (match) {
|
||||||
|
stream.pos = match.index
|
||||||
|
} else {
|
||||||
|
stream.skipToEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -92,7 +127,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
'Cmd-T': function (cm) {
|
'Cmd-T': function (cm) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
},
|
},
|
||||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||||
'Ctrl-C': (cm) => {
|
'Ctrl-C': (cm) => {
|
||||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
@@ -107,6 +142,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.on('blur', this.blurHandler)
|
this.editor.on('blur', this.blurHandler)
|
||||||
this.editor.on('change', this.changeHandler)
|
this.editor.on('change', this.changeHandler)
|
||||||
this.editor.on('paste', this.pasteHandler)
|
this.editor.on('paste', this.pasteHandler)
|
||||||
|
eventEmitter.on('top:search', this.searchHandler)
|
||||||
|
|
||||||
|
eventEmitter.emit('code:init')
|
||||||
|
this.editor.on('scroll', this.scrollHandler)
|
||||||
|
|
||||||
const editorTheme = document.getElementById('editorTheme')
|
const editorTheme = document.getElementById('editorTheme')
|
||||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||||
@@ -126,6 +165,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.off('blur', this.blurHandler)
|
this.editor.off('blur', this.blurHandler)
|
||||||
this.editor.off('change', this.changeHandler)
|
this.editor.off('change', this.changeHandler)
|
||||||
this.editor.off('paste', this.pasteHandler)
|
this.editor.off('paste', this.pasteHandler)
|
||||||
|
eventEmitter.off('top:search', this.searchHandler)
|
||||||
|
this.editor.off('scroll', this.scrollHandler)
|
||||||
const editorTheme = document.getElementById('editorTheme')
|
const editorTheme = document.getElementById('editorTheme')
|
||||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||||
}
|
}
|
||||||
@@ -231,27 +272,67 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePaste (editor, e) {
|
handlePaste (editor, e) {
|
||||||
const dataTransferItem = e.clipboardData.items[0]
|
const clipboardData = e.clipboardData
|
||||||
if (!dataTransferItem.type.match('image')) return
|
const dataTransferItem = clipboardData.items[0]
|
||||||
|
const pastedTxt = clipboardData.getData('text')
|
||||||
const blob = dataTransferItem.getAsFile()
|
const isURL = (str) => {
|
||||||
const reader = new window.FileReader()
|
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||||
let base64data
|
return matcher.test(str)
|
||||||
|
|
||||||
reader.readAsDataURL(blob)
|
|
||||||
reader.onloadend = () => {
|
|
||||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
|
||||||
base64data += base64data.replace('+', ' ')
|
|
||||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
|
||||||
const imageName = Math.random().toString(36).slice(-16)
|
|
||||||
const storagePath = findStorage(this.props.storageKey).path
|
|
||||||
const imageDir = path.join(storagePath, 'images')
|
|
||||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
|
||||||
const imagePath = path.join(imageDir, `${imageName}.png`)
|
|
||||||
fs.writeFile(imagePath, binaryData, 'binary')
|
|
||||||
const imageMd = `})`
|
|
||||||
this.insertImageMd(imageMd)
|
|
||||||
}
|
}
|
||||||
|
if (dataTransferItem.type.match('image')) {
|
||||||
|
const blob = dataTransferItem.getAsFile()
|
||||||
|
const reader = new FileReader()
|
||||||
|
let base64data
|
||||||
|
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
reader.onloadend = () => {
|
||||||
|
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||||
|
base64data += base64data.replace('+', ' ')
|
||||||
|
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||||
|
const imageName = Math.random().toString(36).slice(-16)
|
||||||
|
const storagePath = findStorage(this.props.storageKey).path
|
||||||
|
const imageDir = path.join(storagePath, 'images')
|
||||||
|
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||||
|
const imagePath = path.join(imageDir, `${imageName}.png`)
|
||||||
|
fs.writeFile(imagePath, binaryData, 'binary')
|
||||||
|
const imageMd = `})`
|
||||||
|
this.insertImageMd(imageMd)
|
||||||
|
}
|
||||||
|
} else if (this.props.fetchUrlTitle && isURL(pastedTxt)) {
|
||||||
|
this.handlePasteUrl(e, editor, pastedTxt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
if (this.props.onScroll) {
|
||||||
|
this.props.onScroll(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePasteUrl (e, editor, pastedTxt) {
|
||||||
|
e.preventDefault()
|
||||||
|
const taggedUrl = `<${pastedTxt}>`
|
||||||
|
editor.replaceSelection(taggedUrl)
|
||||||
|
|
||||||
|
fetch(pastedTxt, {
|
||||||
|
method: 'get'
|
||||||
|
}).then((response) => {
|
||||||
|
return (response.text())
|
||||||
|
}).then((response) => {
|
||||||
|
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html')
|
||||||
|
const value = editor.getValue()
|
||||||
|
const cursor = editor.getCursor()
|
||||||
|
const LinkWithTitle = `[${parsedResponse.title}](${pastedTxt})`
|
||||||
|
const newValue = value.replace(taggedUrl, LinkWithTitle)
|
||||||
|
editor.setValue(newValue)
|
||||||
|
editor.setCursor(cursor)
|
||||||
|
}).catch((e) => {
|
||||||
|
const value = editor.getValue()
|
||||||
|
const newValue = value.replace(taggedUrl, pastedTxt)
|
||||||
|
const cursor = editor.getCursor()
|
||||||
|
editor.setValue(newValue)
|
||||||
|
editor.setCursor(cursor)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import styles from './MarkdownEditor.styl'
|
|||||||
import CodeEditor from 'browser/components/CodeEditor'
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import {findStorage} from 'browser/lib/findStorage'
|
||||||
|
|
||||||
class MarkdownEditor extends React.Component {
|
class MarkdownEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -92,7 +92,9 @@ class MarkdownEditor extends React.Component {
|
|||||||
if (this.state.isLocked) return
|
if (this.state.isLocked) return
|
||||||
this.setState({ keyPressed: new Set() })
|
this.setState({ keyPressed: new Set() })
|
||||||
const { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'BLUR') {
|
if (config.editor.switchPreview === 'BLUR' ||
|
||||||
|
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
|
||||||
|
) {
|
||||||
const cursorPosition = this.refs.code.editor.getCursor()
|
const cursorPosition = this.refs.code.editor.getCursor()
|
||||||
this.setState({
|
this.setState({
|
||||||
status: 'PREVIEW'
|
status: 'PREVIEW'
|
||||||
@@ -104,6 +106,20 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDoubleClick (e) {
|
||||||
|
if (this.state.isLocked) return
|
||||||
|
this.setState({keyPressed: new Set()})
|
||||||
|
const { config } = this.props
|
||||||
|
if (config.editor.switchPreview === 'DBL_CLICK') {
|
||||||
|
this.setState({
|
||||||
|
status: 'CODE'
|
||||||
|
}, () => {
|
||||||
|
this.refs.code.focus()
|
||||||
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handlePreviewMouseDown (e) {
|
handlePreviewMouseDown (e) {
|
||||||
this.previewMouseDownedAt = new Date()
|
this.previewMouseDownedAt = new Date()
|
||||||
}
|
}
|
||||||
@@ -245,6 +261,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
onBlur={(e) => this.handleBlur(e)}
|
onBlur={(e) => this.handleBlur(e)}
|
||||||
/>
|
/>
|
||||||
@@ -264,6 +281,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
scrollPastEnd={config.preview.scrollPastEnd}
|
scrollPastEnd={config.preview.scrollPastEnd}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
|
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
value={this.state.renderValue}
|
value={this.state.renderValue}
|
||||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||||
|
|||||||
113
browser/components/MarkdownPreview.js
Normal file → Executable file
113
browser/components/MarkdownPreview.js
Normal file → Executable file
@@ -9,10 +9,10 @@ import Raphael from 'raphael'
|
|||||||
import flowchart from 'flowchart'
|
import flowchart from 'flowchart'
|
||||||
import SequenceDiagram from 'js-sequence-diagrams'
|
import SequenceDiagram from 'js-sequence-diagrams'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import fs from 'fs'
|
|
||||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import mdurl from 'mdurl'
|
import mdurl from 'mdurl'
|
||||||
|
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { app } = remote
|
const { app } = remote
|
||||||
@@ -23,6 +23,10 @@ const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
|||||||
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
||||||
? app.getAppPath()
|
? app.getAppPath()
|
||||||
: path.resolve())
|
: path.resolve())
|
||||||
|
const CSS_FILES = [
|
||||||
|
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||||
|
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||||
|
]
|
||||||
|
|
||||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd) {
|
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd) {
|
||||||
return `
|
return `
|
||||||
@@ -116,6 +120,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||||
|
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
||||||
|
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||||
@@ -146,13 +152,21 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.props.onCheckboxClick(e)
|
this.props.onCheckboxClick(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
if (this.props.onScroll) {
|
||||||
|
this.props.onScroll(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleContextMenu (e) {
|
handleContextMenu (e) {
|
||||||
if (!this.props.onContextMenu) return
|
|
||||||
this.props.onContextMenu(e)
|
this.props.onContextMenu(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDoubleClick (e) {
|
||||||
|
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
handleMouseDown (e) {
|
handleMouseDown (e) {
|
||||||
if (!this.props.onMouseDown) return
|
|
||||||
if (e.target != null) {
|
if (e.target != null) {
|
||||||
switch (e.target.tagName) {
|
switch (e.target.tagName) {
|
||||||
case 'A':
|
case 'A':
|
||||||
@@ -180,8 +194,33 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsHtml () {
|
handleSaveAsHtml () {
|
||||||
this.exportAsDocument('html', (value) => {
|
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||||
return this.refs.root.contentWindow.document.documentElement.outerHTML
|
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
|
||||||
|
|
||||||
|
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
||||||
|
const body = markdown.render(noteContent)
|
||||||
|
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
file = file.replace('file://', '')
|
||||||
|
exportTasks.push({
|
||||||
|
src: file,
|
||||||
|
dst: 'css'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let styles = ''
|
||||||
|
files.forEach((file) => {
|
||||||
|
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||||
|
})
|
||||||
|
|
||||||
|
return `<html>
|
||||||
|
<head>
|
||||||
|
<style id="style">${inlineStyles}</style>
|
||||||
|
${styles}
|
||||||
|
</head>
|
||||||
|
<body>${body}</body>
|
||||||
|
</html>`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,23 +228,29 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.contentWindow.print()
|
this.refs.root.contentWindow.print()
|
||||||
}
|
}
|
||||||
|
|
||||||
exportAsDocument (fileType, formatter) {
|
exportAsDocument (fileType, contentFormatter) {
|
||||||
const options = {
|
const options = {
|
||||||
filters: [
|
filters: [
|
||||||
{ name: 'Documents', extensions: [fileType] }
|
{name: 'Documents', extensions: [fileType]}
|
||||||
],
|
],
|
||||||
properties: ['openFile', 'createDirectory']
|
properties: ['openFile', 'createDirectory']
|
||||||
}
|
}
|
||||||
const value = formatter ? formatter.call(this, this.props.value) : this.props.value
|
|
||||||
|
|
||||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
||||||
(filename) => {
|
(filename) => {
|
||||||
if (filename) {
|
if (filename) {
|
||||||
fs.writeFile(filename, value, (err) => {
|
const content = this.props.value
|
||||||
if (err) throw err
|
const storage = this.props.storagePath
|
||||||
|
|
||||||
|
exportNote(storage, content, filename, contentFormatter)
|
||||||
|
.then((res) => {
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
||||||
|
}).catch((err) => {
|
||||||
|
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fixDecodedURI (node) {
|
fixDecodedURI (node) {
|
||||||
@@ -222,20 +267,26 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.head.innerHTML = `
|
let styles = `
|
||||||
<style id='style'></style>
|
<style id='style'></style>
|
||||||
<link rel="stylesheet" href="${appPath}/node_modules/katex/dist/katex.min.css">
|
|
||||||
<link rel="stylesheet" href="${appPath}/node_modules/codemirror/lib/codemirror.css">
|
|
||||||
<link rel="stylesheet" id="codeTheme">
|
<link rel="stylesheet" id="codeTheme">
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
`
|
`
|
||||||
|
|
||||||
|
CSS_FILES.forEach((file) => {
|
||||||
|
styles += `<link rel="stylesheet" href="${file}">`
|
||||||
|
})
|
||||||
|
|
||||||
|
this.refs.root.contentWindow.document.head.innerHTML = styles
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler)
|
||||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||||
@@ -246,8 +297,10 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler)
|
||||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||||
@@ -269,25 +322,31 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyle () {
|
getStyleParams () {
|
||||||
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props
|
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props
|
||||||
let { fontFamily, codeBlockFontFamily } = this.props
|
let { fontFamily, codeBlockFontFamily } = this.props
|
||||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||||
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
||||||
: defaultFontFamily
|
: defaultFontFamily
|
||||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||||
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
||||||
: defaultCodeBlockFontFamily
|
: defaultCodeBlockFontFamily
|
||||||
|
|
||||||
this.setCodeTheme(codeBlockTheme)
|
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyStyle () {
|
||||||
|
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd} = this.getStyleParams()
|
||||||
|
|
||||||
|
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
||||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd)
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
setCodeTheme (theme) {
|
GetCodeThemeLink (theme) {
|
||||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
||||||
? theme
|
? theme
|
||||||
: 'elegant'
|
: 'elegant'
|
||||||
this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized')
|
return theme.startsWith('solarized')
|
||||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
||||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
||||||
}
|
}
|
||||||
@@ -317,10 +376,6 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => {
|
|
||||||
el.parentNode.parentNode.style.listStyleType = 'none'
|
|
||||||
})
|
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||||
this.fixDecodedURI(el)
|
this.fixDecodedURI(el)
|
||||||
el.addEventListener('click', this.anchorClickHandler)
|
el.addEventListener('click', this.anchorClickHandler)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import CodeEditor from 'browser/components/CodeEditor'
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import styles from './MarkdownSplitEditor.styl'
|
import styles from './MarkdownSplitEditor.styl'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
@@ -12,6 +13,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
this.value = props.value
|
this.value = props.value
|
||||||
this.focus = () => this.refs.code.focus()
|
this.focus = () => this.refs.code.focus()
|
||||||
this.reload = () => this.refs.code.reload()
|
this.reload = () => this.refs.code.reload()
|
||||||
|
this.userScroll = true
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOnChange () {
|
handleOnChange () {
|
||||||
@@ -19,6 +21,49 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
this.props.onChange()
|
this.props.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
||||||
|
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||||
|
let srcTop, srcHeight, targetTop, targetHeight
|
||||||
|
|
||||||
|
if (this.userScroll) {
|
||||||
|
if (e.doc) {
|
||||||
|
srcTop = _.get(e, 'doc.scrollTop')
|
||||||
|
srcHeight = _.get(e, 'doc.height')
|
||||||
|
targetTop = _.get(previewDoc, 'body.scrollTop')
|
||||||
|
targetHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||||
|
} else {
|
||||||
|
srcTop = _.get(previewDoc, 'body.scrollTop')
|
||||||
|
srcHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||||
|
targetTop = _.get(codeDoc, 'scrollTop')
|
||||||
|
targetHeight = _.get(codeDoc, 'height')
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = (targetHeight * srcTop / srcHeight) - targetTop
|
||||||
|
const framerate = 1000 / 60
|
||||||
|
const frames = 20
|
||||||
|
const refractory = frames * framerate
|
||||||
|
|
||||||
|
this.userScroll = false
|
||||||
|
|
||||||
|
let frame = 0
|
||||||
|
let scrollPos, time
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
time = frame / frames
|
||||||
|
scrollPos = time < 0.5
|
||||||
|
? 2 * time * time // ease in
|
||||||
|
: -1 + (4 - 2 * time) * time // ease out
|
||||||
|
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
|
||||||
|
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance)
|
||||||
|
if (frame >= frames) {
|
||||||
|
clearInterval(timer)
|
||||||
|
setTimeout(() => { this.userScroll = true }, refractory)
|
||||||
|
}
|
||||||
|
frame++
|
||||||
|
}, framerate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleCheckboxClick (e) {
|
handleCheckboxClick (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -66,8 +111,10 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
onChange={this.handleOnChange.bind(this)}
|
onChange={this.handleOnChange.bind(this)}
|
||||||
|
onScroll={this.handleScroll.bind(this)}
|
||||||
/>
|
/>
|
||||||
<MarkdownPreview
|
<MarkdownPreview
|
||||||
style={previewStyle}
|
style={previewStyle}
|
||||||
@@ -84,6 +131,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
tabInde='0'
|
tabInde='0'
|
||||||
value={value}
|
value={value}
|
||||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||||
|
onScroll={this.handleScroll.bind(this)}
|
||||||
showCopyNotification={config.ui.showCopyNotification}
|
showCopyNotification={config.ui.showCopyNotification}
|
||||||
storagePath={storage.path}
|
storagePath={storage.path}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -46,11 +46,22 @@ const TagElementList = (tags) => {
|
|||||||
* @param {Function} handleDragStart
|
* @param {Function} handleDragStart
|
||||||
* @param {string} dateDisplay
|
* @param {string} dateDisplay
|
||||||
*/
|
*/
|
||||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
const NoteItem = ({
|
||||||
|
isActive,
|
||||||
|
note,
|
||||||
|
dateDisplay,
|
||||||
|
handleNoteClick,
|
||||||
|
handleNoteContextMenu,
|
||||||
|
handleDragStart,
|
||||||
|
pathname,
|
||||||
|
storageName,
|
||||||
|
folderName,
|
||||||
|
viewType
|
||||||
|
}) => (
|
||||||
<div styleName={isActive
|
<div styleName={isActive
|
||||||
? 'item--active'
|
? 'item--active'
|
||||||
: 'item'
|
: 'item'
|
||||||
}
|
}
|
||||||
key={`${note.storage}-${note.key}`}
|
key={`${note.storage}-${note.key}`}
|
||||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||||
@@ -68,23 +79,33 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteCont
|
|||||||
: <span styleName='item-title-empty'>Empty</span>
|
: <span styleName='item-title-empty'>Empty</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'>
|
||||||
|
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||||
|
<div styleName='item-middle-app-meta'>
|
||||||
|
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'>
|
||||||
|
{viewType === 'ALL' && storageName}
|
||||||
|
{viewType === 'STORAGE' && folderName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
<div styleName='item-bottom-time'>{dateDisplay}</div>
|
|
||||||
{note.isStarred
|
|
||||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
|
||||||
}
|
|
||||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
|
||||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
|
||||||
}
|
|
||||||
{note.type === 'MARKDOWN_NOTE'
|
|
||||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
<div styleName='item-bottom'>
|
<div styleName='item-bottom'>
|
||||||
<div styleName='item-bottom-tagList'>
|
<div styleName='item-bottom-tagList'>
|
||||||
{note.tags.length > 0
|
{note.tags.length > 0
|
||||||
? TagElementList(note.tags)
|
? TagElementList(note.tags)
|
||||||
: <span styleName='item-bottom-tagList-empty' />
|
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>No tags</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{note.isStarred
|
||||||
|
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||||
|
}
|
||||||
|
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
||||||
|
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||||
|
}
|
||||||
|
{note.type === 'MARKDOWN_NOTE'
|
||||||
|
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||||
|
: ''
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,6 +90,26 @@ $control-height = 30px
|
|||||||
font-weight normal
|
font-weight normal
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-middle
|
||||||
|
font-size 13px
|
||||||
|
padding-left 2px
|
||||||
|
padding-bottom 2px
|
||||||
|
|
||||||
|
.item-middle-time
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
display inline-block
|
||||||
|
|
||||||
|
.item-middle-app-meta
|
||||||
|
float right
|
||||||
|
.item-middle-app-meta-label
|
||||||
|
opacity 0.4
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
padding 0 3px
|
||||||
|
white-space nowrap
|
||||||
|
text-overflow ellipsis
|
||||||
|
overflow hidden
|
||||||
|
max-width 200px
|
||||||
|
|
||||||
.item-bottom
|
.item-bottom
|
||||||
position relative
|
position relative
|
||||||
bottom 0px
|
bottom 0px
|
||||||
@@ -124,9 +144,7 @@ $control-height = 30px
|
|||||||
padding-bottom 2px
|
padding-bottom 2px
|
||||||
|
|
||||||
.item-star
|
.item-star
|
||||||
position absolute
|
position relative
|
||||||
right -6px
|
|
||||||
bottom 23px
|
|
||||||
width 16px
|
width 16px
|
||||||
height 16px
|
height 16px
|
||||||
color alpha($ui-favorite-star-button-color, 60%)
|
color alpha($ui-favorite-star-button-color, 60%)
|
||||||
@@ -135,9 +153,7 @@ $control-height = 30px
|
|||||||
border-radius 17px
|
border-radius 17px
|
||||||
|
|
||||||
.item-pin
|
.item-pin
|
||||||
position absolute
|
position relative
|
||||||
right 0px
|
|
||||||
bottom 2px
|
|
||||||
width 34px
|
width 34px
|
||||||
height 34px
|
height 34px
|
||||||
color #E54D42
|
color #E54D42
|
||||||
@@ -192,7 +208,7 @@ body[data-theme="dark"]
|
|||||||
.item-bottom-tagList-item
|
.item-bottom-tagList-item
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
background-color alpha(white, 10%)
|
background-color alpha(white, 10%)
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.item-wrapper
|
.item-wrapper
|
||||||
border-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
border-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||||
@@ -266,7 +282,7 @@ body[data-theme="solarized-dark"]
|
|||||||
.item-bottom-tagList-item
|
.item-bottom-tagList-item
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 10%)
|
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 10%)
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
.item-wrapper
|
.item-wrapper
|
||||||
border-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
|
border-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
|
||||||
@@ -304,4 +320,4 @@ body[data-theme="solarized-dark"]
|
|||||||
|
|
||||||
.item-bottom-tagList-empty
|
.item-bottom-tagList-empty
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
|
|||||||
@@ -14,11 +14,20 @@ import styles from './NoteItemSimple.styl'
|
|||||||
* @param {Function} handleNoteContextMenu
|
* @param {Function} handleNoteContextMenu
|
||||||
* @param {Function} handleDragStart
|
* @param {Function} handleDragStart
|
||||||
*/
|
*/
|
||||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
const NoteItemSimple = ({
|
||||||
|
isActive,
|
||||||
|
isAllNotesView,
|
||||||
|
note,
|
||||||
|
handleNoteClick,
|
||||||
|
handleNoteContextMenu,
|
||||||
|
handleDragStart,
|
||||||
|
pathname,
|
||||||
|
storage
|
||||||
|
}) => (
|
||||||
<div styleName={isActive
|
<div styleName={isActive
|
||||||
? 'item-simple--active'
|
? 'item-simple--active'
|
||||||
: 'item-simple'
|
: 'item-simple'
|
||||||
}
|
}
|
||||||
key={`${note.storage}-${note.key}`}
|
key={`${note.storage}-${note.key}`}
|
||||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||||
@@ -30,7 +39,7 @@ const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu
|
|||||||
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
|
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
|
||||||
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
||||||
}
|
}
|
||||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||||
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
@@ -38,6 +47,11 @@ const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu
|
|||||||
? note.title
|
? note.title
|
||||||
: <span styleName='item-simple-title-empty'>Empty</span>
|
: <span styleName='item-simple-title-empty'>Empty</span>
|
||||||
}
|
}
|
||||||
|
{isAllNotesView && <div styleName='item-simple-right'>
|
||||||
|
<span styleName='item-simple-right-storageName'>
|
||||||
|
{storage.name}
|
||||||
|
</span>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ body[data-theme="dark"]
|
|||||||
.item-simple-bottom-tagList-item
|
.item-simple-bottom-tagList-item
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
background-color alpha(white, 10%)
|
background-color alpha(white, 10%)
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.item-simple--active
|
.item-simple--active
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
@@ -188,7 +188,7 @@ body[data-theme="solarized-dark"]
|
|||||||
.item-simple-bottom-tagList-item
|
.item-simple-bottom-tagList-item
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
background-color alpha(white, 10%)
|
background-color alpha(white, 10%)
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
.item-simple--active
|
.item-simple--active
|
||||||
border-color $ui-solarized-dark-borderColor
|
border-color $ui-solarized-dark-borderColor
|
||||||
@@ -206,4 +206,9 @@ body[data-theme="solarized-dark"]
|
|||||||
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||||
color #c0392b
|
color #c0392b
|
||||||
.item-simple-bottom-tagList-item
|
.item-simple-bottom-tagList-item
|
||||||
background-color alpha(#fff, 20%)
|
background-color alpha(#fff, 20%)
|
||||||
|
.item-simple-right
|
||||||
|
float right
|
||||||
|
.item-simple-right-storageName
|
||||||
|
padding-left 4px
|
||||||
|
opacity 0.4
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import styles from './SideNavFilter.styl'
|
|||||||
const SideNavFilter = ({
|
const SideNavFilter = ({
|
||||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
isFolded, isHomeActive, handleAllNotesButtonClick,
|
||||||
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
||||||
counterTotalNote, counterStarredNote
|
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||||
|
|
||||||
@@ -26,9 +26,9 @@ const SideNavFilter = ({
|
|||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isHomeActive
|
<img src={isHomeActive
|
||||||
? '../resources/icon/icon-all-active.svg'
|
? '../resources/icon/icon-all-active.svg'
|
||||||
: '../resources/icon/icon-all.svg'
|
: '../resources/icon/icon-all.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span styleName='menu-button-label'>All Notes</span>
|
<span styleName='menu-button-label'>All Notes</span>
|
||||||
@@ -40,9 +40,9 @@ const SideNavFilter = ({
|
|||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isStarredActive
|
<img src={isStarredActive
|
||||||
? '../resources/icon/icon-star-active.svg'
|
? '../resources/icon/icon-star-active.svg'
|
||||||
: '../resources/icon/icon-star-sidenav.svg'
|
: '../resources/icon/icon-star-sidenav.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span styleName='menu-button-label'>Starred</span>
|
<span styleName='menu-button-label'>Starred</span>
|
||||||
@@ -54,12 +54,12 @@ const SideNavFilter = ({
|
|||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isTrashedActive
|
<img src={isTrashedActive
|
||||||
? '../resources/icon/icon-trash-active.svg'
|
? '../resources/icon/icon-trash-active.svg'
|
||||||
: '../resources/icon/icon-trash-sidenav.svg'
|
: '../resources/icon/icon-trash-sidenav.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span styleName='menu-button-label'>Trash</span>
|
<span onContextMenu={handleFilterButtonContextMenu} styleName='menu-button-label'>Trash</span>
|
||||||
<span styleName='counters'>{counterDelNote}</span>
|
<span styleName='counters'>{counterDelNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
* @param {bool} isActive
|
* @param {bool} isActive
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TagListItem = ({name, handleClickTagListItem, isActive}) => (
|
const TagListItem = ({name, handleClickTagListItem, isActive, count}) => (
|
||||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||||
<span styleName='tagList-item-name'>
|
<span styleName='tagList-item-name'>
|
||||||
{`# ${name}`}
|
{`# ${name}`}
|
||||||
|
<span styleName='tagList-item-count'> {count}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,6 +48,9 @@
|
|||||||
overflow hidden
|
overflow hidden
|
||||||
text-overflow ellipsis
|
text-overflow ellipsis
|
||||||
|
|
||||||
|
.tagList-item-count
|
||||||
|
padding 0 3px
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.tagList-item
|
.tagList-item
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
@@ -63,6 +66,8 @@ body[data-theme="white"]
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
&:hover
|
&:hover
|
||||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||||
|
.tagList-item-count
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.tagList-item
|
.tagList-item
|
||||||
@@ -81,4 +86,6 @@ body[data-theme="dark"]
|
|||||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||||
|
.tagList-item-count
|
||||||
|
color $ui-dark-button--active-color
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ body
|
|||||||
justify-content left
|
justify-content left
|
||||||
li
|
li
|
||||||
label.taskListItem
|
label.taskListItem
|
||||||
margin-left -2em
|
margin-left -1.8em
|
||||||
&.checked
|
&.checked
|
||||||
text-decoration line-through
|
text-decoration line-through
|
||||||
opacity 0.5
|
opacity 0.5
|
||||||
@@ -178,6 +178,8 @@ ul
|
|||||||
margin-bottom 1em
|
margin-bottom 1em
|
||||||
li
|
li
|
||||||
display list-item
|
display list-item
|
||||||
|
&.taskListItem
|
||||||
|
list-style none
|
||||||
p
|
p
|
||||||
margin 0
|
margin 0
|
||||||
&>li>ul, &>li>ol
|
&>li>ul, &>li>ol
|
||||||
@@ -218,6 +220,7 @@ pre
|
|||||||
background-color white
|
background-color white
|
||||||
&.CodeMirror
|
&.CodeMirror
|
||||||
height initial
|
height initial
|
||||||
|
flex-wrap wrap
|
||||||
&>code
|
&>code
|
||||||
flex 1
|
flex 1
|
||||||
overflow-x auto
|
overflow-x auto
|
||||||
@@ -227,6 +230,13 @@ pre
|
|||||||
padding 0
|
padding 0
|
||||||
border none
|
border none
|
||||||
border-radius 0
|
border-radius 0
|
||||||
|
&>span.filename
|
||||||
|
width 100%
|
||||||
|
border-radius: 5px 0px 0px 0px
|
||||||
|
margin -8px 100% 8px -8px
|
||||||
|
padding 0px 6px
|
||||||
|
background-color #777;
|
||||||
|
color white
|
||||||
&>span.lineNumber
|
&>span.lineNumber
|
||||||
display none
|
display none
|
||||||
font-size 1em
|
font-size 1em
|
||||||
|
|||||||
0
browser/finder/NoteDetail.js
Normal file
0
browser/finder/NoteDetail.js
Normal file
@@ -3,15 +3,17 @@ import emoji from 'markdown-it-emoji'
|
|||||||
import math from '@rokt33r/markdown-it-math'
|
import math from '@rokt33r/markdown-it-math'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
import {lastFindInArray} from './utils'
|
||||||
|
|
||||||
// FIXME We should not depend on global variable.
|
// FIXME We should not depend on global variable.
|
||||||
const katex = window.katex
|
const katex = window.katex
|
||||||
const config = ConfigManager.get()
|
const config = ConfigManager.get()
|
||||||
|
|
||||||
function createGutter (str) {
|
function createGutter (str, firstLineNumber) {
|
||||||
const lc = (str.match(/\n/g) || []).length
|
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||||
|
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
|
||||||
const lines = []
|
const lines = []
|
||||||
for (let i = 1; i <= lc; i++) {
|
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
|
||||||
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
||||||
}
|
}
|
||||||
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
||||||
@@ -24,15 +26,22 @@ var md = markdownit({
|
|||||||
xhtmlOut: true,
|
xhtmlOut: true,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
highlight: function (str, lang) {
|
highlight: function (str, lang) {
|
||||||
if (lang === 'flowchart') {
|
const delimiter = ':'
|
||||||
|
const langInfo = lang.split(delimiter)
|
||||||
|
const langType = langInfo[0]
|
||||||
|
const fileName = langInfo[1] || ''
|
||||||
|
const firstLineNumber = parseInt(langInfo[2], 10)
|
||||||
|
|
||||||
|
if (langType === 'flowchart') {
|
||||||
return `<pre class="flowchart">${str}</pre>`
|
return `<pre class="flowchart">${str}</pre>`
|
||||||
}
|
}
|
||||||
if (lang === 'sequence') {
|
if (langType === 'sequence') {
|
||||||
return `<pre class="sequence">${str}</pre>`
|
return `<pre class="sequence">${str}</pre>`
|
||||||
}
|
}
|
||||||
return '<pre class="code">' +
|
return '<pre class="code">' +
|
||||||
createGutter(str) +
|
'<span class="filename">' + fileName + '</span>' +
|
||||||
'<code class="' + lang + '">' +
|
createGutter(str, firstLineNumber) +
|
||||||
|
'<code class="' + langType + '">' +
|
||||||
str +
|
str +
|
||||||
'</code></pre>'
|
'</code></pre>'
|
||||||
}
|
}
|
||||||
@@ -125,6 +134,13 @@ md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
|||||||
if (state.parentType === 'list') {
|
if (state.parentType === 'list') {
|
||||||
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||||
if (match) {
|
if (match) {
|
||||||
|
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
|
||||||
|
if (liToken) {
|
||||||
|
if (!liToken.attrs) {
|
||||||
|
liToken.attrs = []
|
||||||
|
}
|
||||||
|
liToken.attrs.push(['class', 'taskListItem'])
|
||||||
|
}
|
||||||
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
browser/lib/utils.js
Normal file
11
browser/lib/utils.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export function lastFindInArray (array, callback) {
|
||||||
|
for (let i = array.length - 1; i >= 0; --i) {
|
||||||
|
if (callback(array[i], i, array)) {
|
||||||
|
return array[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
lastFindInArray
|
||||||
|
}
|
||||||
18
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
18
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
@@ -19,6 +19,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
|||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import TrashButton from './TrashButton'
|
import TrashButton from './TrashButton'
|
||||||
import FullscreenButton from './FullscreenButton'
|
import FullscreenButton from './FullscreenButton'
|
||||||
|
import RestoreButton from './RestoreButton'
|
||||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||||
import InfoButton from './InfoButton'
|
import InfoButton from './InfoButton'
|
||||||
import ToggleModeButton from './ToggleModeButton'
|
import ToggleModeButton from './ToggleModeButton'
|
||||||
@@ -68,11 +69,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
if (this.saveQueue != null) this.saveNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUnmount () {
|
|
||||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
|
if (this.saveQueue != null) this.saveNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateTag () {
|
handleUpdateTag () {
|
||||||
@@ -198,8 +196,9 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
noteKey: data.noteKey
|
noteKey: data.noteKey
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ee.once('list:moved', dispatchHandler)
|
ee.once('list:next', dispatchHandler)
|
||||||
})
|
})
|
||||||
|
.then(() => ee.emit('list:next'))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (confirmDeletion()) {
|
if (confirmDeletion()) {
|
||||||
@@ -323,10 +322,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
const trashTopBar = <div styleName='info'>
|
const trashTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<i styleName='undo-button'
|
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||||
className='fa fa-undo fa-fw'
|
|
||||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
@@ -361,12 +357,10 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
value={this.state.note.tags}
|
value={this.state.note.tags}
|
||||||
onChange={this.handleUpdateTag.bind(this)}
|
onChange={this.handleUpdateTag.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
|
||||||
|
|
||||||
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
|
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||||
<StarButton
|
<StarButton
|
||||||
onClick={(e) => this.handleStarButtonClick(e)}
|
onClick={(e) => this.handleStarButtonClick(e)}
|
||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
box-shadow none
|
box-shadow none
|
||||||
padding 20px 40px
|
padding 20px 40px
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
.lock-button
|
.lock-button
|
||||||
padding-bottom 3px
|
padding-bottom 3px
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
margin 0 30px
|
margin 0 30px
|
||||||
.body-noteEditor
|
.body-noteEditor
|
||||||
absolute top bottom left right
|
absolute top bottom left right
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.root
|
.root
|
||||||
box-shadow $note-detail-box-shadow
|
box-shadow $note-detail-box-shadow
|
||||||
|
|||||||
21
browser/main/Detail/RestoreButton.js
Normal file
21
browser/main/Detail/RestoreButton.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './RestoreButton.styl'
|
||||||
|
|
||||||
|
const RestoreButton = ({
|
||||||
|
onClick
|
||||||
|
}) => (
|
||||||
|
<button styleName='control-restoreButton'
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
|
||||||
|
<span styleName='tooltip'>Restore</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
RestoreButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(RestoreButton, styles)
|
||||||
22
browser/main/Detail/RestoreButton.styl
Normal file
22
browser/main/Detail/RestoreButton.styl
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.control-restoreButton
|
||||||
|
top 115px
|
||||||
|
topBarButtonRight()
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
left 25px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.control-restoreButton
|
||||||
|
topBarButtonDark()
|
||||||
@@ -20,6 +20,7 @@ import _ from 'lodash'
|
|||||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import TrashButton from './TrashButton'
|
import TrashButton from './TrashButton'
|
||||||
|
import RestoreButton from './RestoreButton'
|
||||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||||
import InfoButton from './InfoButton'
|
import InfoButton from './InfoButton'
|
||||||
import InfoPanel from './InfoPanel'
|
import InfoPanel from './InfoPanel'
|
||||||
@@ -191,8 +192,9 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
noteKey: data.noteKey
|
noteKey: data.noteKey
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ee.once('list:moved', dispatchHandler)
|
ee.once('list:next', dispatchHandler)
|
||||||
})
|
})
|
||||||
|
.then(() => ee.emit('list:next'))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (confirmDeletion()) {
|
if (confirmDeletion()) {
|
||||||
@@ -567,6 +569,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
keyMap={config.editor.keyMap}
|
keyMap={config.editor.keyMap}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||||
ref={'code-' + index}
|
ref={'code-' + index}
|
||||||
/>
|
/>
|
||||||
@@ -587,10 +590,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
const trashTopBar = <div styleName='info'>
|
const trashTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<i styleName='undo-button'
|
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||||
className='fa fa-undo fa-fw'
|
|
||||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
width 52px
|
width 52px
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
position absolute
|
position: relative
|
||||||
right 165px
|
top 2px
|
||||||
.active
|
.active
|
||||||
background-color #1EC38B
|
background-color #1EC38B
|
||||||
width 33px
|
width 33px
|
||||||
@@ -55,4 +55,4 @@ body[data-theme="solarized-dark"]
|
|||||||
background-color #002B36
|
background-color #002B36
|
||||||
.active
|
.active
|
||||||
background-color #1EC38B
|
background-color #1EC38B
|
||||||
box-shadow 2px 0px 7px #222222
|
box-shadow 2px 0px 7px #222222
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ class NoteList extends React.Component {
|
|||||||
this.deleteNote = this.deleteNote.bind(this)
|
this.deleteNote = this.deleteNote.bind(this)
|
||||||
this.focusNote = this.focusNote.bind(this)
|
this.focusNote = this.focusNote.bind(this)
|
||||||
this.pinToTop = this.pinToTop.bind(this)
|
this.pinToTop = this.pinToTop.bind(this)
|
||||||
|
this.getNoteStorage = this.getNoteStorage.bind(this)
|
||||||
|
this.getNoteFolder = this.getNoteFolder.bind(this)
|
||||||
|
this.getViewType = this.getViewType.bind(this)
|
||||||
|
this.restoreNote = this.restoreNote.bind(this)
|
||||||
|
|
||||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -109,14 +113,27 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
|
const { selectedNoteKeys } = this.state
|
||||||
|
const visibleNoteKeys = this.notes.map(note => `${note.storage}-${note.key}`)
|
||||||
|
const note = this.notes[0]
|
||||||
|
const prevKey = prevProps.location.query.key
|
||||||
|
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && `${note.storage}-${note.key}`
|
||||||
|
|
||||||
if (this.notes.length > 0 && location.query.key == null) {
|
if (note && location.query.key == null) {
|
||||||
const { router } = this.context
|
const { router } = this.context
|
||||||
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
||||||
|
|
||||||
|
// A visible note is an active note
|
||||||
|
if (!selectedNoteKeys.includes(noteKey)) {
|
||||||
|
if (selectedNoteKeys.length === 1) selectedNoteKeys.pop()
|
||||||
|
selectedNoteKeys.push(noteKey)
|
||||||
|
ee.emit('list:moved')
|
||||||
|
}
|
||||||
|
|
||||||
router.replace({
|
router.replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: this.notes[0].storage + '-' + this.notes[0].key
|
key: noteKey
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -440,14 +457,23 @@ class NoteList extends React.Component {
|
|||||||
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top'
|
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top'
|
||||||
const deleteLabel = 'Delete Note'
|
const deleteLabel = 'Delete Note'
|
||||||
const cloneNote = 'Clone Note'
|
const cloneNote = 'Clone Note'
|
||||||
|
const restoreNote = 'Restore Note'
|
||||||
|
|
||||||
const menu = new Menu()
|
const menu = new Menu()
|
||||||
if (!location.pathname.match(/\/home|\/starred|\/trash/)) {
|
if (!location.pathname.match(/\/starred|\/trash/)) {
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: pinLabel,
|
label: pinLabel,
|
||||||
click: this.pinToTop
|
click: this.pinToTop
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (location.pathname.match(/\/trash/)) {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: restoreNote,
|
||||||
|
click: this.restoreNote
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: deleteLabel,
|
label: deleteLabel,
|
||||||
click: this.deleteNote
|
click: this.deleteNote
|
||||||
@@ -459,28 +485,50 @@ class NoteList extends React.Component {
|
|||||||
menu.popup()
|
menu.popup()
|
||||||
}
|
}
|
||||||
|
|
||||||
pinToTop () {
|
updateSelectedNotes (updateFunc, cleanSelection = true) {
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
|
||||||
|
if (!_.isFunction(updateFunc)) {
|
||||||
|
console.warn('Update function is not defined. No update will happen')
|
||||||
|
updateFunc = (note) => { return note }
|
||||||
|
}
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
selectedNotes.map((note) => {
|
selectedNotes.map((note) => {
|
||||||
note.isPinned = !note.isPinned
|
note = updateFunc(note)
|
||||||
return dataApi
|
return dataApi
|
||||||
.updateNote(note.storage, note.key, note)
|
.updateNote(note.storage, note.key, note)
|
||||||
})
|
|
||||||
)
|
|
||||||
.then((updatedNotes) => {
|
|
||||||
updatedNotes.forEach((note) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_NOTE',
|
|
||||||
note
|
|
||||||
})
|
})
|
||||||
})
|
)
|
||||||
|
.then((updatedNotes) => {
|
||||||
|
updatedNotes.forEach((note) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (cleanSelection) {
|
||||||
|
this.selectNextNote()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pinToTop () {
|
||||||
|
this.updateSelectedNotes((note) => {
|
||||||
|
note.isPinned = !note.isPinned
|
||||||
|
return note
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreNote () {
|
||||||
|
this.updateSelectedNotes((note) => {
|
||||||
|
note.isTrashed = false
|
||||||
|
return note
|
||||||
})
|
})
|
||||||
this.setState({ selectedNoteKeys: [] })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteNote () {
|
deleteNote () {
|
||||||
@@ -678,6 +726,24 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNoteStorage (note) { // note.storage = storage key
|
||||||
|
return this.props.data.storageMap.toJS()[note.storage]
|
||||||
|
}
|
||||||
|
|
||||||
|
getNoteFolder (note) { // note.folder = folder key
|
||||||
|
return _.find(this.getNoteStorage(note).folders, ({ key }) => key === note.folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewType () {
|
||||||
|
const { pathname } = this.props.location
|
||||||
|
const folder = /\/folders\/[a-zA-Z0-9]+/.test(pathname)
|
||||||
|
const storage = /\/storages\/[a-zA-Z0-9]+/.test(pathname) && !folder
|
||||||
|
const allNotes = pathname === '/home'
|
||||||
|
if (allNotes) return 'ALL'
|
||||||
|
if (folder) return 'FOLDER'
|
||||||
|
if (storage) return 'STORAGE'
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { location, config } = this.props
|
const { location, config } = this.props
|
||||||
let { notes } = this.props
|
let { notes } = this.props
|
||||||
@@ -687,7 +753,7 @@ class NoteList extends React.Component {
|
|||||||
: config.sortBy === 'ALPHABETICAL'
|
: config.sortBy === 'ALPHABETICAL'
|
||||||
? sortByAlphabetical
|
? sortByAlphabetical
|
||||||
: sortByUpdatedAt
|
: sortByUpdatedAt
|
||||||
const sortedNotes = location.pathname.match(/\/home|\/starred|\/trash/)
|
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||||
? this.getNotes().sort(sortFunc)
|
? this.getNotes().sort(sortFunc)
|
||||||
: this.sortByPin(this.getNotes().sort(sortFunc))
|
: this.sortByPin(this.getNotes().sort(sortFunc))
|
||||||
this.notes = notes = sortedNotes.filter((note) => {
|
this.notes = notes = sortedNotes.filter((note) => {
|
||||||
@@ -714,6 +780,8 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const viewType = this.getViewType()
|
||||||
|
|
||||||
const noteList = notes
|
const noteList = notes
|
||||||
.map(note => {
|
.map(note => {
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
@@ -739,6 +807,9 @@ class NoteList extends React.Component {
|
|||||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||||
handleDragStart={this.handleDragStart.bind(this)}
|
handleDragStart={this.handleDragStart.bind(this)}
|
||||||
pathname={location.pathname}
|
pathname={location.pathname}
|
||||||
|
folderName={this.getNoteFolder(note).name}
|
||||||
|
storageName={this.getNoteStorage(note).name}
|
||||||
|
viewType={viewType}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -752,6 +823,9 @@ class NoteList extends React.Component {
|
|||||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||||
handleDragStart={this.handleDragStart.bind(this)}
|
handleDragStart={this.handleDragStart.bind(this)}
|
||||||
pathname={location.pathname}
|
pathname={location.pathname}
|
||||||
|
folderName={this.getNoteFolder(note).name}
|
||||||
|
storageName={this.getNoteStorage(note).name}
|
||||||
|
viewType={viewType}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -766,16 +840,17 @@ class NoteList extends React.Component {
|
|||||||
<div styleName='control-sortBy'>
|
<div styleName='control-sortBy'>
|
||||||
<i className='fa fa-angle-down' />
|
<i className='fa fa-angle-down' />
|
||||||
<select styleName='control-sortBy-select'
|
<select styleName='control-sortBy-select'
|
||||||
|
title='Select filter mode'
|
||||||
value={config.sortBy}
|
value={config.sortBy}
|
||||||
onChange={(e) => this.handleSortByChange(e)}
|
onChange={(e) => this.handleSortByChange(e)}
|
||||||
>
|
>
|
||||||
<option value='UPDATED_AT'>Updated</option>
|
<option title='Sort by update time' value='UPDATED_AT'>Updated</option>
|
||||||
<option value='CREATED_AT'>Created</option>
|
<option title='Sort by create time' value='CREATED_AT'>Created</option>
|
||||||
<option value='ALPHABETICAL'>Alphabetically</option>
|
<option title='Sort alphabetically' value='ALPHABETICAL'>Alphabetically</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='control-button-area'>
|
<div styleName='control-button-area'>
|
||||||
<button styleName={config.listStyle === 'DEFAULT'
|
<button title='Default View' styleName={config.listStyle === 'DEFAULT'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
@@ -783,7 +858,7 @@ class NoteList extends React.Component {
|
|||||||
>
|
>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
||||||
</button>
|
</button>
|
||||||
<button styleName={config.listStyle === 'SMALL'
|
<button title='Compressed View' styleName={config.listStyle === 'SMALL'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
const { remote } = require('electron')
|
||||||
|
const { Menu } = remote
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import styles from './SideNav.styl'
|
import styles from './SideNav.styl'
|
||||||
import { openModal } from 'browser/main/lib/modal'
|
import { openModal } from 'browser/main/lib/modal'
|
||||||
import PreferencesModal from '../modals/PreferencesModal'
|
import PreferencesModal from '../modals/PreferencesModal'
|
||||||
@@ -86,9 +89,10 @@ class SideNav extends React.Component {
|
|||||||
isTrashedActive={isTrashedActive}
|
isTrashedActive={isTrashedActive}
|
||||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
||||||
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
||||||
counterTotalNote={data.noteMap._map.size}
|
counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
|
||||||
counterStarredNote={data.starredSet._set.size}
|
counterStarredNote={data.starredSet._set.size}
|
||||||
counterDelNote={data.trashedSet._set.size}
|
counterDelNote={data.trashedSet._set.size}
|
||||||
|
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StorageList storageList={storageList} />
|
<StorageList storageList={storageList} />
|
||||||
@@ -113,18 +117,21 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
tagListComponent () {
|
tagListComponent () {
|
||||||
const { data, location } = this.props
|
const { data, location } = this.props
|
||||||
const tagList = data.tagNoteMap.map((tag, key) => {
|
const tagList = data.tagNoteMap.map((tag, name) => {
|
||||||
return key
|
return { name, size: tag.size }
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
tagList.map(tag => (
|
tagList.map(tag => {
|
||||||
<TagListItem
|
return (
|
||||||
name={tag}
|
<TagListItem
|
||||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
name={tag.name}
|
||||||
isActive={this.getTagActive(location.pathname, tag)}
|
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||||
key={tag}
|
isActive={this.getTagActive(location.pathname, tag)}
|
||||||
/>
|
key={tag.name}
|
||||||
))
|
count={tag.size}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +146,34 @@ class SideNav extends React.Component {
|
|||||||
router.push(`/tags/${name}`)
|
router.push(`/tags/${name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emptyTrash (entries) {
|
||||||
|
const { dispatch } = this.props
|
||||||
|
const deletionPromises = entries.map((storageAndNoteKey) => {
|
||||||
|
const storageKey = storageAndNoteKey.split('-')[0]
|
||||||
|
const noteKey = storageAndNoteKey.split('-')[1]
|
||||||
|
return dataApi.deleteNote(storageKey, noteKey)
|
||||||
|
})
|
||||||
|
Promise.all(deletionPromises)
|
||||||
|
.then((arrayOfStorageAndNoteKeys) => {
|
||||||
|
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||||
|
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Cannot Delete note: ' + err)
|
||||||
|
})
|
||||||
|
console.log('Trash emptied')
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilterButtonContextMenu (event) {
|
||||||
|
const { data } = this.props
|
||||||
|
const entries = data.trashedSet.toJS()
|
||||||
|
const menu = Menu.buildFromTemplate([
|
||||||
|
{ label: 'Empty Trash', click: () => this.emptyTrash(entries) }
|
||||||
|
])
|
||||||
|
menu.popup()
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { data, location, config, dispatch } = this.props
|
const { data, location, config, dispatch } = this.props
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,18 @@ class TopBar extends React.Component {
|
|||||||
this.focusSearchHandler = () => {
|
this.focusSearchHandler = () => {
|
||||||
this.handleOnSearchFocus()
|
this.handleOnSearchFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.codeInitHandler = this.handleCodeInit.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
ee.on('top:focus-search', this.focusSearchHandler)
|
ee.on('top:focus-search', this.focusSearchHandler)
|
||||||
|
ee.on('code:init', this.codeInitHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
ee.off('top:focus-search', this.focusSearchHandler)
|
ee.off('top:focus-search', this.focusSearchHandler)
|
||||||
|
ee.off('code:init', this.codeInitHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
@@ -73,14 +77,16 @@ class TopBar extends React.Component {
|
|||||||
|
|
||||||
handleSearchChange (e) {
|
handleSearchChange (e) {
|
||||||
const { router } = this.context
|
const { router } = this.context
|
||||||
|
const keyword = this.refs.searchInput.value
|
||||||
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
||||||
router.push('/searched')
|
router.push('/searched')
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
search: this.refs.searchInput.value
|
search: keyword
|
||||||
})
|
})
|
||||||
|
ee.emit('top:search', keyword)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchFocus (e) {
|
handleSearchFocus (e) {
|
||||||
@@ -115,6 +121,10 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCodeInit () {
|
||||||
|
ee.emit('top:search', this.refs.searchInput.value)
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { config, style, location } = this.props
|
const { config, style, location } = this.props
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export const DEFAULT_CONFIG = {
|
|||||||
displayLineNumbers: true,
|
displayLineNumbers: true,
|
||||||
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
||||||
scrollPastEnd: false,
|
scrollPastEnd: false,
|
||||||
type: 'SPLIT'
|
type: 'SPLIT',
|
||||||
|
fetchUrlTitle: true
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
fontSize: '14',
|
fontSize: '14',
|
||||||
|
|||||||
31
browser/main/lib/dataApi/copyFile.js
Executable file
31
browser/main/lib/dataApi/copyFile.js
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Copy a file from source to destination
|
||||||
|
* @param {String} srcPath
|
||||||
|
* @param {String} dstPath
|
||||||
|
* @return {Promise} an image path
|
||||||
|
*/
|
||||||
|
function copyFile (srcPath, dstPath) {
|
||||||
|
if (!path.extname(dstPath)) {
|
||||||
|
dstPath = path.join(dstPath, path.basename(srcPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const dstFolder = path.dirname(dstPath)
|
||||||
|
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
||||||
|
|
||||||
|
const input = fs.createReadStream(srcPath)
|
||||||
|
const output = fs.createWriteStream(dstPath)
|
||||||
|
|
||||||
|
output.on('error', reject)
|
||||||
|
input.on('error', reject)
|
||||||
|
input.on('end', () => {
|
||||||
|
resolve(dstPath)
|
||||||
|
})
|
||||||
|
input.pipe(output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = copyFile
|
||||||
110
browser/main/lib/dataApi/exportNote.js
Executable file
110
browser/main/lib/dataApi/exportNote.js
Executable file
@@ -0,0 +1,110 @@
|
|||||||
|
import copyFile from 'browser/main/lib/dataApi/copyFile'
|
||||||
|
import {findStorage} from 'browser/lib/findStorage'
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const LOCAL_STORED_REGEX = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
||||||
|
const IMAGES_FOLDER_NAME = 'images'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export note together with images
|
||||||
|
*
|
||||||
|
* If images is stored in the storage, creates 'images' subfolder in target directory
|
||||||
|
* and copies images to it. Changes links to images in the content of the note
|
||||||
|
*
|
||||||
|
* @param {String} storageKey or storage path
|
||||||
|
* @param {String} noteContent Content to export
|
||||||
|
* @param {String} targetPath Path to exported file
|
||||||
|
* @param {function} outputFormatter
|
||||||
|
* @return {Promise.<*[]>}
|
||||||
|
*/
|
||||||
|
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
||||||
|
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
||||||
|
const exportTasks = []
|
||||||
|
|
||||||
|
if (!storagePath) {
|
||||||
|
throw new Error('Storage path is not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => {
|
||||||
|
if (!path.extname(dstFilename)) {
|
||||||
|
dstFilename += path.extname(srcFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename)
|
||||||
|
|
||||||
|
exportTasks.push({
|
||||||
|
src: path.join(IMAGES_FOLDER_NAME, srcFilename),
|
||||||
|
dst: dstRelativePath
|
||||||
|
})
|
||||||
|
|
||||||
|
return ``
|
||||||
|
})
|
||||||
|
|
||||||
|
if (outputFormatter) {
|
||||||
|
exportedData = outputFormatter(exportedData, exportTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
||||||
|
|
||||||
|
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
|
||||||
|
.then(() => {
|
||||||
|
return saveToFile(exportedData, targetPath)
|
||||||
|
}).catch((err) => {
|
||||||
|
rollbackExport(tasks)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareTasks (tasks, storagePath, targetPath) {
|
||||||
|
return tasks.map((task) => {
|
||||||
|
if (!path.isAbsolute(task.src)) {
|
||||||
|
task.src = path.join(storagePath, task.src)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.isAbsolute(task.dst)) {
|
||||||
|
task.dst = path.join(targetPath, task.dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
return task
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveToFile (data, filename) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.writeFile(filename, data, (err) => {
|
||||||
|
if (err) return reject(err)
|
||||||
|
|
||||||
|
resolve(filename)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove exported files
|
||||||
|
* @param tasks Array of copy task objects. Object consists of two mandatory fields – `src` and `dst`
|
||||||
|
*/
|
||||||
|
function rollbackExport (tasks) {
|
||||||
|
const folders = new Set()
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
let fullpath = task.dst
|
||||||
|
|
||||||
|
if (!path.extname(task.dst)) {
|
||||||
|
fullpath = path.join(task.dst, path.basename(task.src))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(fullpath)) {
|
||||||
|
fs.unlink(fullpath)
|
||||||
|
folders.add(path.dirname(fullpath))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
if (fs.readdirSync(folder).length === 0) {
|
||||||
|
fs.rmdir(folder)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default exportNote
|
||||||
@@ -18,7 +18,7 @@ nodeIpc.connectTo(
|
|||||||
console.log(err)
|
console.log(err)
|
||||||
})
|
})
|
||||||
nodeIpc.of.node.on('connect', function () {
|
nodeIpc.of.node.on('connect', function () {
|
||||||
console.log('Conncted successfully')
|
console.log('Connected successfully')
|
||||||
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
|
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
|
||||||
})
|
})
|
||||||
nodeIpc.of.node.on('disconnect', function () {
|
nodeIpc.of.node.on('disconnect', function () {
|
||||||
|
|||||||
@@ -35,14 +35,16 @@ class NewNoteModal extends React.Component {
|
|||||||
content: ''
|
content: ''
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
|
const noteHash = `${note.storage}-${note.key}`
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
hashHistory.push({
|
hashHistory.push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {key: note.storage + '-' + note.key}
|
query: {key: noteHash}
|
||||||
})
|
})
|
||||||
|
ee.emit('list:jump', noteHash)
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
this.props.close()
|
this.props.close()
|
||||||
})
|
})
|
||||||
@@ -73,14 +75,16 @@ class NewNoteModal extends React.Component {
|
|||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
|
const noteHash = `${note.storage}-${note.key}`
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
hashHistory.push({
|
hashHistory.push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {key: note.storage + '-' + note.key}
|
query: {key: noteHash}
|
||||||
})
|
})
|
||||||
|
ee.emit('list:jump', noteHash)
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
this.props.close()
|
this.props.close()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class InfoTab extends React.Component {
|
|||||||
>GitHub</a>
|
>GitHub</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href='https://medium.com/boostnote'
|
<a href='https://boostlog.io/@junp1234'
|
||||||
onClick={(e) => this.handleLinkClick(e)}
|
onClick={(e) => this.handleLinkClick(e)}
|
||||||
>Blog</a>
|
>Blog</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -128,7 +128,7 @@ class InfoTab extends React.Component {
|
|||||||
>Development</a> : Development configurations for Boostnote.
|
>Development</a> : Development configurations for Boostnote.
|
||||||
</li>
|
</li>
|
||||||
<li styleName='cc'>
|
<li styleName='cc'>
|
||||||
Copyright (C) 2017 Maisin&Co.
|
Copyright (C) 2017 - 2018 BoostIO
|
||||||
</li>
|
</li>
|
||||||
<li styleName='cc'>
|
<li styleName='cc'>
|
||||||
License: GPL v3
|
License: GPL v3
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ class UiTab extends React.Component {
|
|||||||
displayLineNumbers: this.refs.editorDisplayLineNumbers.checked,
|
displayLineNumbers: this.refs.editorDisplayLineNumbers.checked,
|
||||||
switchPreview: this.refs.editorSwitchPreview.value,
|
switchPreview: this.refs.editorSwitchPreview.value,
|
||||||
keyMap: this.refs.editorKeyMap.value,
|
keyMap: this.refs.editorKeyMap.value,
|
||||||
scrollPastEnd: this.refs.scrollPastEnd.checked
|
scrollPastEnd: this.refs.scrollPastEnd.checked,
|
||||||
|
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
fontSize: this.refs.previewFontSize.value,
|
fontSize: this.refs.previewFontSize.value,
|
||||||
@@ -283,6 +284,7 @@ class UiTab extends React.Component {
|
|||||||
onChange={(e) => this.handleUIChange(e)}
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
>
|
>
|
||||||
<option value='BLUR'>When Editor Blurred</option>
|
<option value='BLUR'>When Editor Blurred</option>
|
||||||
|
<option value='DBL_CLICK'>When Editor Blurred, Edit On Double Click</option>
|
||||||
<option value='RIGHTCLICK'>On Right Click</option>
|
<option value='RIGHTCLICK'>On Right Click</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,6 +329,17 @@ class UiTab extends React.Component {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div styleName='group-checkBoxSection'>
|
||||||
|
<label>
|
||||||
|
<input onChange={(e) => this.handleUIChange(e)}
|
||||||
|
checked={this.state.config.editor.fetchUrlTitle}
|
||||||
|
ref='editorFetchUrlTitle'
|
||||||
|
type='checkbox'
|
||||||
|
/>
|
||||||
|
Bring in web page title when pasting URL on editor
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div styleName='group-header2'>Preview</div>
|
<div styleName='group-header2'>Preview</div>
|
||||||
<div styleName='group-section'>
|
<div styleName='group-section'>
|
||||||
<div styleName='group-section-label'>
|
<div styleName='group-section-label'>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Thank you for your help in advance.
|
|||||||
|
|
||||||
### About copyright of Pull Request
|
### About copyright of Pull Request
|
||||||
|
|
||||||
If you make a pull request, It means you agree to transfer the copyright of the code changes to Maisin&Co.
|
If you make a pull request, It means you agree to transfer the copyright of the code changes to BoostIO.
|
||||||
|
|
||||||
It doesn't mean Boostnote will become a paid app. If we want to earn some money, We will try other way, which is some kind of cloud storage, Mobile app integration or some SPECIAL features.
|
It doesn't mean Boostnote will become a paid app. If we want to earn some money, We will try other way, which is some kind of cloud storage, Mobile app integration or some SPECIAL features.
|
||||||
Because GPL v3 is too strict to be compatible with any other License, We thought this is needed to replace the license with much freer one(like BSD, MIT) somewhen.
|
Because GPL v3 is too strict to be compatible with any other License, We thought this is needed to replace the license with much freer one(like BSD, MIT) somewhen.
|
||||||
@@ -27,7 +27,7 @@ Because GPL v3 is too strict to be compatible with any other License, We thought
|
|||||||
|
|
||||||
### Об авторских правах Pull Request
|
### Об авторских правах Pull Request
|
||||||
|
|
||||||
Если вы делаете pull request, значит вы согласны передать авторские права на изменения кода в Maisin&Co.
|
Если вы делаете pull request, значит вы согласны передать авторские права на изменения кода в BoostIO.
|
||||||
|
|
||||||
Это не означает, что Boostnote станет платным приложением. Если мы захотим заработать немного денег, мы найдем другой способ. Например, использование облачного хранилища, интеграцией мобильных приложений или другими специальными функциями.
|
Это не означает, что Boostnote станет платным приложением. Если мы захотим заработать немного денег, мы найдем другой способ. Например, использование облачного хранилища, интеграцией мобильных приложений или другими специальными функциями.
|
||||||
Так как лицензия GPL v3 слишком строгая, чтобы быть совместимой с любой другой лицензией, мы думаем, что нужно заменить лицензию на более свободную (например, BSD, MIT).
|
Так как лицензия GPL v3 слишком строгая, чтобы быть совместимой с любой другой лицензией, мы думаем, что нужно заменить лицензию на более свободную (например, BSD, MIT).
|
||||||
@@ -45,7 +45,7 @@ Because GPL v3 is too strict to be compatible with any other License, We thought
|
|||||||
|
|
||||||
### Pull Request의 저작권에 관하여
|
### Pull Request의 저작권에 관하여
|
||||||
|
|
||||||
당신이 pull request를 요청하면, 코드 변경에 대한 저작권을 Maisin&Co에 양도한다는 것에 동의한다는 의미입니다.
|
당신이 pull request를 요청하면, 코드 변경에 대한 저작권을 BoostIO에 양도한다는 것에 동의한다는 의미입니다.
|
||||||
|
|
||||||
이것은 Boostnote가 유료화가 되는 것을 의미하는 건 아닙니다. 만약 우리가 자금이 필요하다면, 우리는 클라우드 연동, 모바일 앱 통합 혹은 특수한 기능 같은 것을 사용해 수입 창출을 시도할 것입니다.
|
이것은 Boostnote가 유료화가 되는 것을 의미하는 건 아닙니다. 만약 우리가 자금이 필요하다면, 우리는 클라우드 연동, 모바일 앱 통합 혹은 특수한 기능 같은 것을 사용해 수입 창출을 시도할 것입니다.
|
||||||
GPL v3 라이센스는 다른 라이센스와 혼합해 사용하기엔 너무 엄격하므로, 우리는 BSD, MIT 라이센스와 같은 더 자유로운 라이센스로 교체하는 것을 생각하고 있습니다.
|
GPL v3 라이센스는 다른 라이센스와 혼합해 사용하기엔 너무 엄격하므로, 우리는 BSD, MIT 라이센스와 같은 더 자유로운 라이센스로 교체하는 것을 생각하고 있습니다.
|
||||||
@@ -63,7 +63,7 @@ GPL v3 라이센스는 다른 라이센스와 혼합해 사용하기엔 너무
|
|||||||
|
|
||||||
### Pull requestの著作権について
|
### Pull requestの著作権について
|
||||||
|
|
||||||
Pull requestをすることはその変化分のコードの著作権をMaisin&Co.に譲渡することに同意することになります。
|
Pull requestをすることはその変化分のコードの著作権をBoostIOに譲渡することに同意することになります。
|
||||||
|
|
||||||
アプリケーションのLicenseをいつでも変える選択肢を残したいと思うからです。
|
アプリケーションのLicenseをいつでも変える選択肢を残したいと思うからです。
|
||||||
これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。
|
これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。
|
||||||
@@ -83,7 +83,7 @@ Pull requestをすることはその変化分のコードの著作権をMaisin&C
|
|||||||
感谢您对我们的支持。
|
感谢您对我们的支持。
|
||||||
|
|
||||||
### 关于您提供的Pull Request的著作权(版权)问题
|
### 关于您提供的Pull Request的著作权(版权)问题
|
||||||
如果您提供了一个Pull Request,这表示您将您所修改的代码的著作权移交给Maisin&Co。
|
如果您提供了一个Pull Request,这表示您将您所修改的代码的著作权移交给BoostIO。
|
||||||
|
|
||||||
这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益,我们会尝试一些其他的方法,比如说云存储、绑定手机软件等。
|
这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益,我们会尝试一些其他的方法,比如说云存储、绑定手机软件等。
|
||||||
因为GPLv3过于严格,不能和其他的一些协议兼容,所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议,比如说BSD、MIT。
|
因为GPLv3过于严格,不能和其他的一些协议兼容,所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议,比如说BSD、MIT。
|
||||||
|
|||||||
@@ -40,20 +40,16 @@ mainWindow.webContents.sendInputEvent({
|
|||||||
keyCode: '\u0008'
|
keyCode: '\u0008'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (process.platform !== 'linux' || process.env.DESKTOP_SESSION === 'cinnamon') {
|
if (process.platform === 'darwin' || process.env.DESKTOP_SESSION === 'cinnamon') {
|
||||||
mainWindow.on('close', function (e) {
|
mainWindow.on('close', function (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (process.platform === 'win32') {
|
if (mainWindow.isFullScreen()) {
|
||||||
quitApp()
|
mainWindow.once('leave-full-screen', function () {
|
||||||
} else {
|
|
||||||
if (mainWindow.isFullScreen()) {
|
|
||||||
mainWindow.once('leave-full-screen', function () {
|
|
||||||
mainWindow.hide()
|
|
||||||
})
|
|
||||||
mainWindow.setFullScreen(false)
|
|
||||||
} else {
|
|
||||||
mainWindow.hide()
|
mainWindow.hide()
|
||||||
}
|
})
|
||||||
|
mainWindow.setFullScreen(false)
|
||||||
|
} else {
|
||||||
|
mainWindow.hide()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -66,9 +62,6 @@ if (process.platform !== 'linux' || process.env.DESKTOP_SESSION === 'cinnamon')
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
mainWindow.on('resize', _.throttle(storeWindowSize, 500))
|
mainWindow.on('resize', _.throttle(storeWindowSize, 500))
|
||||||
function quitApp () {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
function storeWindowSize () {
|
function storeWindowSize () {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<script src="../node_modules/codemirror/keymap/emacs.js"></script>
|
<script src="../node_modules/codemirror/keymap/emacs.js"></script>
|
||||||
<script src="../node_modules/codemirror/addon/runmode/runmode.js"></script>
|
<script src="../node_modules/codemirror/addon/runmode/runmode.js"></script>
|
||||||
|
|
||||||
<script src="../node_modules/codemirror/addon/edit/continuelist.js"></script>
|
<script src="../node_modules/boost/boostNewLineIndentContinueMarkdownList.js"></script>
|
||||||
|
|
||||||
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
|
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
|
||||||
|
|
||||||
|
|||||||
46
node_modules/boost/boostNewLineIndentContinueMarkdownList.js
generated
vendored
Normal file
46
node_modules/boost/boostNewLineIndentContinueMarkdownList.js
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../codemirror/lib/codemirror"));
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../codemirror/lib/codemirror"], mod);
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror);
|
||||||
|
})(function(CodeMirror) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/,
|
||||||
|
emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/,
|
||||||
|
unorderedListRE = /[*+-]\s/;
|
||||||
|
|
||||||
|
CodeMirror.commands.boostNewLineAndIndentContinueMarkdownList = function(cm) {
|
||||||
|
if (cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||||
|
var ranges = cm.listSelections(), replacements = [];
|
||||||
|
for (var i = 0; i < ranges.length; i++) {
|
||||||
|
var pos = ranges[i].head;
|
||||||
|
var eolState = cm.getStateAfter(pos.line);
|
||||||
|
var inList = eolState.list !== false;
|
||||||
|
var inQuote = eolState.quote !== 0;
|
||||||
|
var line = cm.getLine(pos.line), match = listRE.exec(line);
|
||||||
|
if (!ranges[i].empty() || (!inList && !inQuote) || !match || pos.ch < match[2].length - 1) {
|
||||||
|
cm.execCommand("newlineAndIndent");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (emptyListRE.test(line)) {
|
||||||
|
if (!/>\s*$/.test(line)) cm.replaceRange("", {
|
||||||
|
line: pos.line, ch: 0
|
||||||
|
}, {
|
||||||
|
line: pos.line, ch: pos.ch + 1
|
||||||
|
});
|
||||||
|
replacements[i] = "\n";
|
||||||
|
} else {
|
||||||
|
var indent = match[1], after = match[5];
|
||||||
|
var bullet = unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0
|
||||||
|
? match[2].replace("x", " ")
|
||||||
|
: (parseInt(match[3], 10) + 1) + match[4];
|
||||||
|
replacements[i] = "\n" + indent + bullet + after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.replaceSelections(replacements);
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "boost",
|
"name": "boost",
|
||||||
"productName": "Boostnote",
|
"productName": "Boostnote",
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"description": "Boostnote",
|
"description": "Boostnote",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Boostnote is an open source project. It's an independent project with its ongoin
|
|||||||
- [Facebook Group](https://www.facebook.com/groups/boostnote/)
|
- [Facebook Group](https://www.facebook.com/groups/boostnote/)
|
||||||
- [Twitter](https://twitter.com/boostnoteapp)
|
- [Twitter](https://twitter.com/boostnoteapp)
|
||||||
- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzAzMjI1MTIyNTQ3LTc2MjNiYWU3NTc1YjZlMTk3NzFmOWE1ZWU1MGRhMzBkMGIwMWFjOWMxMDRiM2I2NzkzYzc4OGZhNmVhZjYzZTM)
|
- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzAzMjI1MTIyNTQ3LTc2MjNiYWU3NTc1YjZlMTk3NzFmOWE1ZWU1MGRhMzBkMGIwMWFjOWMxMDRiM2I2NzkzYzc4OGZhNmVhZjYzZTM)
|
||||||
- [Blog](https://medium.com/boostnote)
|
- [Blog](https://boostlog.io/@junp1234)
|
||||||
- [Reddit](https://www.reddit.com/r/Boostnote/)
|
- [Reddit](https://www.reddit.com/r/Boostnote/)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ var config = Object.assign({}, skeleton, {
|
|||||||
'NODE_ENV': JSON.stringify('production'),
|
'NODE_ENV': JSON.stringify('production'),
|
||||||
'BABEL_ENV': JSON.stringify('production')
|
'BABEL_ENV': JSON.stringify('production')
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
new webpack.optimize.UglifyJsPlugin({
|
|
||||||
compressor: {
|
|
||||||
warnings: false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user