mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
Merge branch 'master' into fixIssue2534
This commit is contained in:
2
.babelrc
2
.babelrc
@@ -7,7 +7,7 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"presets": ["env" ,"react", "es2015"],
|
"presets": ["env" ,"react", "es2015"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
[ "babel-plugin-webpack-alias", { "config": "<rootDir>/webpack.config.js" } ]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# EditorConfig is awesome: http://EditorConfig.org
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
# top-most EditorConfig file
|
# top-most EditorConfig file
|
||||||
root = true
|
root = true
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
"globals": {
|
"globals": {
|
||||||
"FileReader": true,
|
"FileReader": true,
|
||||||
"localStorage": true,
|
"localStorage": true,
|
||||||
"fetch": true
|
"fetch": true,
|
||||||
|
"Image": true,
|
||||||
|
"MutationObserver": true
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"jest": true
|
"jest": true
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
issuehunt: BoostIo/Boostnote
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,4 +9,5 @@ node_modules/*
|
|||||||
/secret
|
/secret
|
||||||
*.log
|
*.log
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 7
|
- 8
|
||||||
script:
|
script:
|
||||||
- npm run lint && npm run test
|
- npm run lint && npm run test
|
||||||
- yarn jest
|
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@6.4 && grunt pre-build; fi'
|
||||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
|
||||||
after_success:
|
after_success:
|
||||||
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
||||||
-in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d
|
-in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d
|
||||||
|
|||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -17,7 +17,7 @@
|
|||||||
"${workspaceFolder}/index.js"
|
"${workspaceFolder}/index.js"
|
||||||
],
|
],
|
||||||
"windows": {
|
"windows": {
|
||||||
"runtimeExecutable": "${workspaceFolder}/node_modeules/.bin/electron.cmd"
|
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
72
Backers.md
72
Backers.md
@@ -1,72 +0,0 @@
|
|||||||
<h1 align="center">Sponsors & Backers</h1>
|
|
||||||
|
|
||||||
Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome backers. If you'd like to join them, please consider:
|
|
||||||
|
|
||||||
- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backers via OpenCollective
|
|
||||||
|
|
||||||
### [Gold Sponsors / $1,000 per month](https://opencollective.com/boostnoteio/order/2259)
|
|
||||||
- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/.
|
|
||||||
|
|
||||||
### [Silver Sponsors / $250 per month](https://opencollective.com/boostnoteio/order/2257)
|
|
||||||
- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/.
|
|
||||||
|
|
||||||
### [Bronze Sponsors / $50 per month](https://opencollective.com/boostnoteio/order/2258)
|
|
||||||
- Get your name and Url (or E-mail) on Readme.md on GitHub.
|
|
||||||
|
|
||||||
### [Backers3 / $10 per month](https://opencollective.com/boostnoteio/order/2176)
|
|
||||||
- [Ralph03](https://opencollective.com/ralph03)
|
|
||||||
|
|
||||||
- [Nikolas Dan](https://opencollective.com/nikolas-dan)
|
|
||||||
|
|
||||||
### [Backers2 / $5 per month](https://opencollective.com/boostnoteio/order/2175)
|
|
||||||
- [Yeojong Kim](https://twitter.com/yeojoy)
|
|
||||||
|
|
||||||
- [Scotia Draven](https://opencollective.com/scotia-draven)
|
|
||||||
|
|
||||||
- [A. J. Vargas](https://opencollective.com/aj-vargas)
|
|
||||||
|
|
||||||
### [Backers1](https://opencollective.com/boostnoteio/order/2563) and One-time sponsors
|
|
||||||
- Ryosuke Tamura - $30
|
|
||||||
|
|
||||||
- tatoosh11 - $10
|
|
||||||
|
|
||||||
- Alexander Borovkov - $10
|
|
||||||
|
|
||||||
- spoonhoop - $5
|
|
||||||
|
|
||||||
- Drew Williams - $2
|
|
||||||
|
|
||||||
- Andy Shaw - $2
|
|
||||||
|
|
||||||
- mysafesky -$2
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backers via Bountysource
|
|
||||||
https://salt.bountysource.com/teams/boostnote
|
|
||||||
|
|
||||||
- Kuzz - $65
|
|
||||||
|
|
||||||
- Intense Raiden - $45
|
|
||||||
|
|
||||||
- ravy22 - $25
|
|
||||||
|
|
||||||
- trentpolack - $20
|
|
||||||
|
|
||||||
- hikariru - $10
|
|
||||||
|
|
||||||
- kolchan11 - $10
|
|
||||||
|
|
||||||
- RonWalker22 - $10
|
|
||||||
|
|
||||||
- hocchuc - $5
|
|
||||||
|
|
||||||
- Adam - $5
|
|
||||||
|
|
||||||
- Steve - $5
|
|
||||||
|
|
||||||
- evmin - $5
|
|
||||||
29
FAQ.md
Normal file
29
FAQ.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
|
||||||
|
<details><summary>Allowing dangerous HTML tags</summary>
|
||||||
|
|
||||||
|
Sometimes it is useful to allow dangerous HTML tags to add interactivity to your notebook. One of the example is to use details/summary as a way to expand/collaps your todo-list.
|
||||||
|
|
||||||
|
* How to enable:
|
||||||
|
* Go to **Preferences** → **Interface** → **Sanitization** → **Allow dangerous html tags**
|
||||||
|
* Example note: Multiple todo-list
|
||||||
|
* Create new notes
|
||||||
|
* Paste the below code, and you'll see that you can expand/collaps the todo-list, and you can have multiple todo-list in your note.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<details><summary>What I want to do</summary>
|
||||||
|
|
||||||
|
- [x] Create an awesome feature X
|
||||||
|
- [ ] Do my homework
|
||||||
|
|
||||||
|
</details>
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Other questions
|
||||||
|
|
||||||
|
You can ask [here][ISSUES]
|
||||||
|
|
||||||
|
[ISSUES]: https://github.com/BoostIO/Boostnote/issues
|
||||||
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 - 2018 BoostIO
|
Copyright (C) 2017 - 2019 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
|
||||||
|
|||||||
36
PULL_REQUEST_TEMPLATE.md
Normal file
36
PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!--
|
||||||
|
Before submitting this PR, please make sure that:
|
||||||
|
- You have read and understand the contributing.md
|
||||||
|
- You have checked docs/code_style.md for information on code style
|
||||||
|
-->
|
||||||
|
## Description
|
||||||
|
<!--
|
||||||
|
Tell us what your PR does.
|
||||||
|
Please attach a screenshot/ video/gif image describing your PR if possible.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Issue fixed
|
||||||
|
<!--
|
||||||
|
Please list out all issue fixed with this PR here.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please make sure you fill in these checkboxes,
|
||||||
|
your PR will be reviewed faster if we know exactly what it does.
|
||||||
|
|
||||||
|
Change :white_circle: to :radio_button: in all the options that apply
|
||||||
|
-->
|
||||||
|
## Type of changes
|
||||||
|
|
||||||
|
- :white_circle: Bug fix (Change that fixed an issue)
|
||||||
|
- :white_circle: Breaking change (Change that can cause existing functionality to change)
|
||||||
|
- :white_circle: Improvement (Change that improves the code. Maybe performance or development improvement)
|
||||||
|
- :white_circle: Feature (Change that adds new functionality)
|
||||||
|
- :white_circle: Documentation change (Change that modifies documentation. Maybe typo fixes)
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
- :white_circle: My code follows [the project code style](docs/code_style.md)
|
||||||
|
- :white_circle: I have written test for my code and it has been tested
|
||||||
|
- :white_circle: All existing tests have been passed
|
||||||
|
- :white_circle: I have attached a screenshot/video to visualize my change if possible
|
||||||
File diff suppressed because it is too large
Load Diff
5
browser/components/CodeEditor.styl
Normal file
5
browser/components/CodeEditor.styl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.codeEditor-typo
|
||||||
|
text-decoration underline wavy red
|
||||||
|
|
||||||
|
.spellcheck-select
|
||||||
|
border: none
|
||||||
68
browser/components/ColorPicker.js
Normal file
68
browser/components/ColorPicker.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { SketchPicker } from 'react-color'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './ColorPicker.styl'
|
||||||
|
|
||||||
|
const componentHeight = 330
|
||||||
|
|
||||||
|
class ColorPicker extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
color: this.props.color || '#939395'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onColorChange = this.onColorChange.bind(this)
|
||||||
|
this.handleConfirm = this.handleConfirm.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
this.onColorChange(nextProps.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
onColorChange (color) {
|
||||||
|
this.setState({
|
||||||
|
color
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfirm () {
|
||||||
|
this.props.onConfirm(this.state.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { onReset, onCancel, targetRect } = this.props
|
||||||
|
const { color } = this.state
|
||||||
|
|
||||||
|
const clientHeight = document.body.clientHeight
|
||||||
|
const alignX = targetRect.right + 4
|
||||||
|
let alignY = targetRect.top
|
||||||
|
if (targetRect.top + componentHeight > clientHeight) {
|
||||||
|
alignY = targetRect.bottom - componentHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div styleName='colorPicker' style={{top: `${alignY}px`, left: `${alignX}px`}}>
|
||||||
|
<div styleName='cover' onClick={onCancel} />
|
||||||
|
<SketchPicker color={color} onChange={this.onColorChange} />
|
||||||
|
<div styleName='footer'>
|
||||||
|
<button styleName='btn-reset' onClick={onReset}>Reset</button>
|
||||||
|
<button styleName='btn-cancel' onClick={onCancel}>Cancel</button>
|
||||||
|
<button styleName='btn-confirm' onClick={this.handleConfirm}>Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorPicker.propTypes = {
|
||||||
|
color: PropTypes.string,
|
||||||
|
targetRect: PropTypes.object,
|
||||||
|
onConfirm: PropTypes.func.isRequired,
|
||||||
|
onCancel: PropTypes.func.isRequired,
|
||||||
|
onReset: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(ColorPicker, styles)
|
||||||
39
browser/components/ColorPicker.styl
Normal file
39
browser/components/ColorPicker.styl
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.colorPicker
|
||||||
|
position fixed
|
||||||
|
z-index 2
|
||||||
|
display flex
|
||||||
|
flex-direction column
|
||||||
|
|
||||||
|
.cover
|
||||||
|
position fixed
|
||||||
|
top 0
|
||||||
|
right 0
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
|
||||||
|
.footer
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
z-index 2
|
||||||
|
align-items center
|
||||||
|
& > button + button
|
||||||
|
margin-left 10px
|
||||||
|
|
||||||
|
.btn-cancel,
|
||||||
|
.btn-confirm,
|
||||||
|
.btn-reset
|
||||||
|
vertical-align middle
|
||||||
|
height 25px
|
||||||
|
margin-top 2.5px
|
||||||
|
border-radius 2px
|
||||||
|
border none
|
||||||
|
padding 0 5px
|
||||||
|
background-color $default-button-background
|
||||||
|
&:hover
|
||||||
|
background-color $default-button-background--hover
|
||||||
|
.btn-confirm
|
||||||
|
background-color #1EC38B
|
||||||
|
&:hover
|
||||||
|
background-color darken(#1EC38B, 25%)
|
||||||
|
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ 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'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||||
|
|
||||||
class MarkdownEditor extends React.Component {
|
class MarkdownEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -19,10 +20,10 @@ class MarkdownEditor extends React.Component {
|
|||||||
this.supportMdSelectionBold = [16, 17, 186]
|
this.supportMdSelectionBold = [16, 17, 186]
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
|
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE',
|
||||||
renderValue: props.value,
|
renderValue: props.value,
|
||||||
keyPressed: new Set(),
|
keyPressed: new Set(),
|
||||||
isLocked: false
|
isLocked: props.isLocked
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lockEditorCode = () => this.handleLockEditor()
|
this.lockEditorCode = () => this.handleLockEditor()
|
||||||
@@ -31,6 +32,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.value = this.refs.code.value
|
this.value = this.refs.code.value
|
||||||
eventEmitter.on('editor:lock', this.lockEditorCode)
|
eventEmitter.on('editor:lock', this.lockEditorCode)
|
||||||
|
eventEmitter.on('editor:focus', this.focusEditor.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate () {
|
||||||
@@ -46,6 +48,15 @@ class MarkdownEditor extends React.Component {
|
|||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.cancelQueue()
|
this.cancelQueue()
|
||||||
eventEmitter.off('editor:lock', this.lockEditorCode)
|
eventEmitter.off('editor:lock', this.lockEditorCode)
|
||||||
|
eventEmitter.off('editor:focus', this.focusEditor.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
focusEditor () {
|
||||||
|
this.setState({
|
||||||
|
status: 'CODE'
|
||||||
|
}, () => {
|
||||||
|
this.refs.code.focus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
queueRendering (value) {
|
queueRendering (value) {
|
||||||
@@ -75,6 +86,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenu (e) {
|
handleContextMenu (e) {
|
||||||
|
if (this.state.isLocked) return
|
||||||
const { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||||
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
|
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
|
||||||
@@ -107,7 +119,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
status: 'PREVIEW'
|
status: 'PREVIEW'
|
||||||
}, () => {
|
}, () => {
|
||||||
this.refs.preview.focus()
|
this.refs.preview.focus()
|
||||||
this.refs.preview.scrollTo(cursorPosition.line)
|
this.refs.preview.scrollToRow(cursorPosition.line)
|
||||||
})
|
})
|
||||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||||
}
|
}
|
||||||
@@ -147,22 +159,25 @@ class MarkdownEditor extends React.Component {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const idMatch = /checkbox-([0-9]+)/
|
const idMatch = /checkbox-([0-9]+)/
|
||||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
|
||||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
|
||||||
|
const checkReplace = /\[x]/i
|
||||||
|
const uncheckReplace = /\[ ]/
|
||||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
const lines = this.refs.code.value
|
const lines = this.refs.code.value
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|
||||||
const targetLine = lines[lineIndex]
|
const targetLine = lines[lineIndex]
|
||||||
|
let newLine = targetLine
|
||||||
|
|
||||||
if (targetLine.match(checkedMatch)) {
|
if (targetLine.match(checkedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(checkedMatch, '- [ ]')
|
newLine = targetLine.replace(checkReplace, '[ ]')
|
||||||
}
|
}
|
||||||
if (targetLine.match(uncheckedMatch)) {
|
if (targetLine.match(uncheckedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '- [x]')
|
newLine = targetLine.replace(uncheckReplace, '[x]')
|
||||||
}
|
}
|
||||||
this.refs.code.setValue(lines.join('\n'))
|
this.refs.code.setLineContent(lineIndex, newLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +234,28 @@ class MarkdownEditor extends React.Component {
|
|||||||
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
|
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDropImage (dropEvent) {
|
||||||
|
dropEvent.preventDefault()
|
||||||
|
const { storageKey, noteKey } = this.props
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
status: 'CODE'
|
||||||
|
}, () => {
|
||||||
|
this.refs.code.focus()
|
||||||
|
|
||||||
|
this.refs.code.editor.execCommand('goDocEnd')
|
||||||
|
this.refs.code.editor.execCommand('goLineEnd')
|
||||||
|
this.refs.code.editor.execCommand('newlineAndIndent')
|
||||||
|
|
||||||
|
attachmentManagement.handleAttachmentDrop(
|
||||||
|
this.refs.code,
|
||||||
|
storageKey,
|
||||||
|
noteKey,
|
||||||
|
dropEvent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleKeyUp (e) {
|
handleKeyUp (e) {
|
||||||
const keyPressed = this.state.keyPressed
|
const keyPressed = this.state.keyPressed
|
||||||
keyPressed.delete(e.keyCode)
|
keyPressed.delete(e.keyCode)
|
||||||
@@ -230,7 +267,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {className, value, config, storageKey, noteKey} = this.props
|
const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props
|
||||||
|
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
@@ -268,13 +305,26 @@ class MarkdownEditor extends React.Component {
|
|||||||
enableRulers={config.editor.enableRulers}
|
enableRulers={config.editor.enableRulers}
|
||||||
rulers={config.editor.rulers}
|
rulers={config.editor.rulers}
|
||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
lineWrapping
|
||||||
|
matchingPairs={config.editor.matchingPairs}
|
||||||
|
matchingTriples={config.editor.matchingTriples}
|
||||||
|
explodingPairs={config.editor.explodingPairs}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
enableTableEditor={config.editor.enableTableEditor}
|
enableTableEditor={config.editor.enableTableEditor}
|
||||||
|
linesHighlighted={linesHighlighted}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
onBlur={(e) => this.handleBlur(e)}
|
onBlur={(e) => this.handleBlur(e)}
|
||||||
|
spellCheck={config.editor.spellcheck}
|
||||||
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
|
hotkey={config.hotkey}
|
||||||
|
switchPreview={config.editor.switchPreview}
|
||||||
|
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||||
|
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||||
|
prettierConfig={config.editor.prettierConfig}
|
||||||
|
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||||
/>
|
/>
|
||||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||||
? 'preview'
|
? 'preview'
|
||||||
@@ -294,6 +344,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
smartArrows={config.preview.smartArrows}
|
smartArrows={config.preview.smartArrows}
|
||||||
breaks={config.preview.breaks}
|
breaks={config.preview.breaks}
|
||||||
sanitize={config.preview.sanitize}
|
sanitize={config.preview.sanitize}
|
||||||
|
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||||
@@ -308,6 +359,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
customCSS={config.preview.customCSS}
|
customCSS={config.preview.customCSS}
|
||||||
allowCustomCSS={config.preview.allowCustomCSS}
|
allowCustomCSS={config.preview.allowCustomCSS}
|
||||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||||
|
onDrop={(e) => this.handleDropImage(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import consts from 'browser/lib/consts'
|
|||||||
import Raphael from 'raphael'
|
import Raphael from 'raphael'
|
||||||
import flowchart from 'flowchart'
|
import flowchart from 'flowchart'
|
||||||
import mermaidRender from './render/MermaidRender'
|
import mermaidRender from './render/MermaidRender'
|
||||||
import SequenceDiagram from 'js-sequence-diagrams'
|
import SequenceDiagram from '@rokt33r/js-sequence-diagrams'
|
||||||
import Chart from 'chart.js'
|
import Chart from 'chart.js'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||||
@@ -18,12 +18,13 @@ import mdurl from 'mdurl'
|
|||||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import context from 'browser/lib/context'
|
import { render } from 'react-dom'
|
||||||
import i18n from 'browser/lib/i18n'
|
import Carousel from 'react-image-carousel'
|
||||||
import fs from 'fs'
|
import ConfigManager from '../main/lib/ConfigManager'
|
||||||
|
|
||||||
const { remote, shell } = require('electron')
|
const { remote, shell } = require('electron')
|
||||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||||
|
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu
|
||||||
|
|
||||||
const { app } = remote
|
const { app } = remote
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
@@ -31,27 +32,39 @@ const fileUrl = require('file-url')
|
|||||||
|
|
||||||
const dialog = remote.dialog
|
const dialog = remote.dialog
|
||||||
|
|
||||||
const uri2path = require('file-uri-to-path')
|
|
||||||
|
|
||||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||||
const appPath = fileUrl(
|
const appPath = fileUrl(
|
||||||
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
||||||
)
|
)
|
||||||
const CSS_FILES = [
|
const CSS_FILES = [
|
||||||
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
`${appPath}/node_modules/codemirror/lib/codemirror.css`,
|
||||||
|
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
|
||||||
]
|
]
|
||||||
|
|
||||||
function buildStyle (
|
/**
|
||||||
fontFamily,
|
* @param {Object} opts
|
||||||
fontSize,
|
* @param {String} opts.fontFamily
|
||||||
codeBlockFontFamily,
|
* @param {Numberl} opts.fontSize
|
||||||
lineNumber,
|
* @param {String} opts.codeBlockFontFamily
|
||||||
scrollPastEnd,
|
* @param {String} opts.theme
|
||||||
theme,
|
* @param {Boolean} [opts.lineNumber] Should show line number
|
||||||
allowCustomCSS,
|
* @param {Boolean} [opts.scrollPastEnd]
|
||||||
customCSS
|
* @param {Boolean} [opts.allowCustomCSS] Should add custom css
|
||||||
) {
|
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
function buildStyle (opts) {
|
||||||
|
const {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
} = opts
|
||||||
return `
|
return `
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
@@ -81,12 +94,17 @@ function buildStyle (
|
|||||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
${markdownStyle}
|
${markdownStyle}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: '${fontFamily.join("','")}';
|
font-family: '${fontFamily.join("','")}';
|
||||||
font-size: ${fontSize}px;
|
font-size: ${fontSize}px;
|
||||||
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
${scrollPastEnd ? `
|
||||||
|
padding-bottom: 90vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
}
|
}
|
||||||
@media print {
|
@media print {
|
||||||
body {
|
body {
|
||||||
@@ -162,6 +180,10 @@ const scrollBarStyle = `
|
|||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(0, 0, 0, 0.15);
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track-piece {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
const scrollBarDarkStyle = `
|
const scrollBarDarkStyle = `
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@@ -171,6 +193,10 @@ const scrollBarDarkStyle = `
|
|||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track-piece {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
@@ -188,6 +214,19 @@ const defaultCodeBlockFontFamily = [
|
|||||||
'source-code-pro',
|
'source-code-pro',
|
||||||
'monospace'
|
'monospace'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// return the line number of the line that used to generate the specified element
|
||||||
|
// return -1 if the line is not found
|
||||||
|
function getSourceLineNumberByElement (element) {
|
||||||
|
let isHasLineNumber = element.dataset.line !== undefined
|
||||||
|
let parent = element
|
||||||
|
while (!isHasLineNumber && parent.parentElement !== null) {
|
||||||
|
parent = parent.parentElement
|
||||||
|
isHasLineNumber = parent.dataset.line !== undefined
|
||||||
|
}
|
||||||
|
return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1
|
||||||
|
}
|
||||||
|
|
||||||
export default class MarkdownPreview extends React.Component {
|
export default class MarkdownPreview extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -204,9 +243,10 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||||
|
this.saveAsPdfHandler = () => this.handleSaveAsPdf()
|
||||||
this.printHandler = () => this.handlePrint()
|
this.printHandler = () => this.handlePrint()
|
||||||
|
|
||||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
this.linkClickHandler = this.handleLinkClick.bind(this)
|
||||||
this.initMarkdown = this.initMarkdown.bind(this)
|
this.initMarkdown = this.initMarkdown.bind(this)
|
||||||
this.initMarkdown()
|
this.initMarkdown()
|
||||||
}
|
}
|
||||||
@@ -231,30 +271,12 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenu (event) {
|
handleContextMenu (event) {
|
||||||
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
|
const menu = buildMarkdownPreviewContextMenu(this, event)
|
||||||
if (_.isFunction(this.props.onContextMenu)) {
|
const switchPreview = ConfigManager.get().editor.switchPreview
|
||||||
|
if (menu != null && switchPreview !== 'RIGHTCLICK') {
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
} else if (_.isFunction(this.props.onContextMenu)) {
|
||||||
this.props.onContextMenu(event)
|
this.props.onContextMenu(event)
|
||||||
return
|
|
||||||
}
|
|
||||||
// No contextMenu was passed to us -> execute our own link-opener
|
|
||||||
if (event.target.tagName.toLowerCase() === 'a') {
|
|
||||||
const href = event.target.href
|
|
||||||
const isLocalFile = href.startsWith('file:')
|
|
||||||
if (isLocalFile) {
|
|
||||||
const absPath = uri2path(href)
|
|
||||||
try {
|
|
||||||
if (fs.lstatSync(absPath).isFile()) {
|
|
||||||
context.popup([
|
|
||||||
{
|
|
||||||
label: i18n.__('Show in explorer'),
|
|
||||||
click: (e) => shell.showItemInFolder(absPath)
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Error while evaluating if the file is locally available', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,14 +285,28 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseDown (e) {
|
handleMouseDown (e) {
|
||||||
if (e.target != null) {
|
const config = ConfigManager.get()
|
||||||
switch (e.target.tagName) {
|
const clickElement = e.target
|
||||||
case 'A':
|
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV"
|
||||||
case 'INPUT':
|
const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
|
||||||
return null
|
|
||||||
|
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
||||||
|
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
||||||
|
}
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
if (config.editor.type === 'SPLIT') {
|
||||||
|
if (lineNumber !== -1) {
|
||||||
|
eventEmitter.emit('line:jump', lineNumber)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (lineNumber !== -1) {
|
||||||
|
eventEmitter.emit('editor:focus')
|
||||||
|
eventEmitter.emit('line:jump', lineNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
|
|
||||||
|
if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp (e) {
|
handleMouseUp (e) {
|
||||||
@@ -286,95 +322,84 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsMd () {
|
handleSaveAsMd () {
|
||||||
this.exportAsDocument('md', (noteContent, exportTasks) => {
|
this.exportAsDocument('md')
|
||||||
let result = noteContent
|
}
|
||||||
if (this.props && this.props.storagePath && this.props.noteKey) {
|
|
||||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
htmlContentFormatter (noteContent, exportTasks, targetDir) {
|
||||||
noteContent,
|
const {
|
||||||
this.props.storagePath
|
fontFamily,
|
||||||
)
|
fontSize,
|
||||||
attachmentsAbsolutePaths.forEach(attachment => {
|
codeBlockFontFamily,
|
||||||
exportTasks.push({
|
lineNumber,
|
||||||
src: attachment,
|
codeBlockTheme,
|
||||||
dst: attachmentManagement.DESTINATION_FOLDER
|
scrollPastEnd,
|
||||||
})
|
theme,
|
||||||
})
|
allowCustomCSS,
|
||||||
result = attachmentManagement.removeStorageAndNoteReferences(
|
customCSS
|
||||||
noteContent,
|
} = this.getStyleParams()
|
||||||
this.props.noteKey
|
|
||||||
)
|
const inlineStyles = buildStyle({
|
||||||
}
|
fontFamily,
|
||||||
return result
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
})
|
})
|
||||||
|
let body = this.markdown.render(noteContent)
|
||||||
|
body = attachmentManagement.fixLocalURLS(
|
||||||
|
body,
|
||||||
|
this.props.storagePath
|
||||||
|
)
|
||||||
|
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
|
files.forEach(file => {
|
||||||
|
if (global.process.platform === 'win32') {
|
||||||
|
file = file.replace('file:///', '')
|
||||||
|
} else {
|
||||||
|
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>
|
||||||
|
<base href="file://${targetDir}/">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||||
|
<style id="style">${inlineStyles}</style>
|
||||||
|
${styles}
|
||||||
|
</head>
|
||||||
|
<body>${body}</body>
|
||||||
|
</html>`
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsHtml () {
|
handleSaveAsHtml () {
|
||||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir)))
|
||||||
const {
|
}
|
||||||
fontFamily,
|
|
||||||
fontSize,
|
|
||||||
codeBlockFontFamily,
|
|
||||||
lineNumber,
|
|
||||||
codeBlockTheme,
|
|
||||||
scrollPastEnd,
|
|
||||||
theme,
|
|
||||||
allowCustomCSS,
|
|
||||||
customCSS
|
|
||||||
} = this.getStyleParams()
|
|
||||||
|
|
||||||
const inlineStyles = buildStyle(
|
handleSaveAsPdf () {
|
||||||
fontFamily,
|
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
|
||||||
fontSize,
|
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false, javascript: false}})
|
||||||
codeBlockFontFamily,
|
printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir))
|
||||||
lineNumber,
|
return new Promise((resolve, reject) => {
|
||||||
scrollPastEnd,
|
printout.webContents.on('did-finish-load', () => {
|
||||||
theme,
|
printout.webContents.printToPDF({}, (err, data) => {
|
||||||
allowCustomCSS,
|
if (err) reject(err)
|
||||||
customCSS
|
else resolve(data)
|
||||||
)
|
printout.destroy()
|
||||||
let body = this.markdown.render(noteContent)
|
})
|
||||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
|
||||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
|
||||||
noteContent,
|
|
||||||
this.props.storagePath
|
|
||||||
)
|
|
||||||
|
|
||||||
files.forEach(file => {
|
|
||||||
if (global.process.platform === 'win32') {
|
|
||||||
file = file.replace('file:///', '')
|
|
||||||
} else {
|
|
||||||
file = file.replace('file://', '')
|
|
||||||
}
|
|
||||||
exportTasks.push({
|
|
||||||
src: file,
|
|
||||||
dst: 'css'
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
attachmentsAbsolutePaths.forEach(attachment => {
|
|
||||||
exportTasks.push({
|
|
||||||
src: attachment,
|
|
||||||
dst: attachmentManagement.DESTINATION_FOLDER
|
|
||||||
})
|
|
||||||
})
|
|
||||||
body = attachmentManagement.removeStorageAndNoteReferences(
|
|
||||||
body,
|
|
||||||
this.props.noteKey
|
|
||||||
)
|
|
||||||
|
|
||||||
let styles = ''
|
|
||||||
files.forEach(file => {
|
|
||||||
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
|
||||||
})
|
|
||||||
|
|
||||||
return `<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
|
||||||
<style id="style">${inlineStyles}</style>
|
|
||||||
${styles}
|
|
||||||
</head>
|
|
||||||
<body>${body}</body>
|
|
||||||
</html>`
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,8 +417,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
if (filename) {
|
if (filename) {
|
||||||
const content = this.props.value
|
const content = this.props.value
|
||||||
const storage = this.props.storagePath
|
const storage = this.props.storagePath
|
||||||
|
const nodeKey = this.props.noteKey
|
||||||
|
|
||||||
exportNote(storage, content, filename, contentFormatter)
|
exportNote(nodeKey, storage, content, filename, contentFormatter)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@@ -423,6 +449,31 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Convert special characters between three ```
|
||||||
|
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
|
||||||
|
* @returns {string} HTML in which special characters between three ``` have been converted
|
||||||
|
*/
|
||||||
|
escapeHtmlCharactersInCodeTag (splitWithCodeTag) {
|
||||||
|
for (let index = 0; index < splitWithCodeTag.length; index++) {
|
||||||
|
const codeTagRequired = (splitWithCodeTag[index] !== '\`\`\`' && index < splitWithCodeTag.length - 1)
|
||||||
|
if (codeTagRequired) {
|
||||||
|
splitWithCodeTag.splice((index + 1), 0, '\`\`\`')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let inCodeTag = false
|
||||||
|
let result = ''
|
||||||
|
for (let content of splitWithCodeTag) {
|
||||||
|
if (content === '\`\`\`') {
|
||||||
|
inCodeTag = !inCodeTag
|
||||||
|
} else if (inCodeTag) {
|
||||||
|
content = escapeHtmlCharacters(content)
|
||||||
|
}
|
||||||
|
result += content
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
getScrollBarStyle () {
|
getScrollBarStyle () {
|
||||||
const { theme } = this.props
|
const { theme } = this.props
|
||||||
|
|
||||||
@@ -438,6 +489,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
const { onDrop } = this.props
|
||||||
|
|
||||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||||
this.refs.root.contentWindow.document.body.addEventListener(
|
this.refs.root.contentWindow.document.body.addEventListener(
|
||||||
'contextmenu',
|
'contextmenu',
|
||||||
@@ -475,7 +528,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
)
|
)
|
||||||
this.refs.root.contentWindow.document.addEventListener(
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
'drop',
|
'drop',
|
||||||
this.preventImageDroppedHandler
|
onDrop || this.preventImageDroppedHandler
|
||||||
)
|
)
|
||||||
this.refs.root.contentWindow.document.addEventListener(
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
'dragover',
|
'dragover',
|
||||||
@@ -488,10 +541,13 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
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)
|
||||||
|
eventEmitter.on('export:save-pdf', this.saveAsPdfHandler)
|
||||||
eventEmitter.on('print', this.printHandler)
|
eventEmitter.on('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
const { onDrop } = this.props
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.body.removeEventListener(
|
this.refs.root.contentWindow.document.body.removeEventListener(
|
||||||
'contextmenu',
|
'contextmenu',
|
||||||
this.contextMenuHandler
|
this.contextMenuHandler
|
||||||
@@ -510,7 +566,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
)
|
)
|
||||||
this.refs.root.contentWindow.document.removeEventListener(
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
'drop',
|
'drop',
|
||||||
this.preventImageDroppedHandler
|
onDrop || this.preventImageDroppedHandler
|
||||||
)
|
)
|
||||||
this.refs.root.contentWindow.document.removeEventListener(
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
'dragover',
|
'dragover',
|
||||||
@@ -523,20 +579,24 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
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)
|
||||||
|
eventEmitter.off('export:save-pdf', this.saveAsPdfHandler)
|
||||||
eventEmitter.off('print', this.printHandler)
|
eventEmitter.off('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
// actual rewriteIframe function should be called only once
|
||||||
|
let needsRewriteIframe = false
|
||||||
|
if (prevProps.value !== this.props.value) needsRewriteIframe = true
|
||||||
if (
|
if (
|
||||||
prevProps.smartQuotes !== this.props.smartQuotes ||
|
prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||||
prevProps.sanitize !== this.props.sanitize ||
|
prevProps.sanitize !== this.props.sanitize ||
|
||||||
|
prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel ||
|
||||||
prevProps.smartArrows !== this.props.smartArrows ||
|
prevProps.smartArrows !== this.props.smartArrows ||
|
||||||
prevProps.breaks !== this.props.breaks ||
|
prevProps.breaks !== this.props.breaks ||
|
||||||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
|
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
|
||||||
) {
|
) {
|
||||||
this.initMarkdown()
|
this.initMarkdown()
|
||||||
this.rewriteIframe()
|
needsRewriteIframe = true
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
prevProps.fontFamily !== this.props.fontFamily ||
|
prevProps.fontFamily !== this.props.fontFamily ||
|
||||||
@@ -551,8 +611,17 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
prevProps.customCSS !== this.props.customCSS
|
prevProps.customCSS !== this.props.customCSS
|
||||||
) {
|
) {
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
|
needsRewriteIframe = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsRewriteIframe) {
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should scroll to top after selecting another note
|
||||||
|
if (prevProps.noteKey !== this.props.noteKey) {
|
||||||
|
this.scrollTo(0, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStyleParams () {
|
getStyleParams () {
|
||||||
@@ -608,8 +677,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
this.getWindow().document.getElementById(
|
this.getWindow().document.getElementById(
|
||||||
'codeTheme'
|
'codeTheme'
|
||||||
).href = this.GetCodeThemeLink(codeBlockTheme)
|
).href = this.getCodeThemeLink(codeBlockTheme)
|
||||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle({
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
codeBlockFontFamily,
|
codeBlockFontFamily,
|
||||||
@@ -618,17 +687,15 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
theme,
|
theme,
|
||||||
allowCustomCSS,
|
allowCustomCSS,
|
||||||
customCSS
|
customCSS
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
GetCodeThemeLink (theme) {
|
getCodeThemeLink (name) {
|
||||||
theme = consts.THEMES.some(_theme => _theme === theme) &&
|
const theme = consts.THEMES.find(theme => theme.name === name)
|
||||||
theme !== 'default'
|
|
||||||
? theme
|
return theme != null
|
||||||
: 'elegant'
|
? theme.path
|
||||||
return theme.startsWith('solarized')
|
: `${appPath}/node_modules/codemirror/theme/elegant.css`
|
||||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
|
||||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rewriteIframe () {
|
rewriteIframe () {
|
||||||
@@ -653,11 +720,17 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
indentSize,
|
indentSize,
|
||||||
showCopyNotification,
|
showCopyNotification,
|
||||||
storagePath,
|
storagePath,
|
||||||
noteKey
|
noteKey,
|
||||||
|
sanitize,
|
||||||
|
mermaidHTMLLabel
|
||||||
} = this.props
|
} = this.props
|
||||||
let { value, codeBlockTheme } = this.props
|
let { value, codeBlockTheme } = this.props
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||||
|
if (sanitize === 'NONE') {
|
||||||
|
const splitWithCodeTag = value.split('```')
|
||||||
|
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag)
|
||||||
|
}
|
||||||
const renderedHTML = this.markdown.render(value)
|
const renderedHTML = this.markdown.render(value)
|
||||||
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
||||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
|
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
|
||||||
@@ -681,9 +754,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
|
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
|
||||||
? codeBlockTheme
|
|
||||||
: 'default'
|
const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default'
|
||||||
|
|
||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
||||||
@@ -696,6 +769,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
copyIcon.innerHTML =
|
copyIcon.innerHTML =
|
||||||
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||||
copyIcon.onclick = e => {
|
copyIcon.onclick = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
copy(content)
|
copy(content)
|
||||||
if (showCopyNotification) {
|
if (showCopyNotification) {
|
||||||
this.notify('Saved to Clipboard!', {
|
this.notify('Saved to Clipboard!', {
|
||||||
@@ -704,14 +779,11 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
el.parentNode.appendChild(copyIcon)
|
el.parentNode.appendChild(copyIcon)
|
||||||
el.innerHTML = ''
|
el.innerHTML = ''
|
||||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
el.parentNode.className += ` ${codeBlockThemeClassName}`
|
||||||
const [refThema, color] = codeBlockTheme.split(' ')
|
|
||||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
|
||||||
} else {
|
|
||||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
|
||||||
}
|
|
||||||
CodeMirror.runMode(content, syntax.mime, el, {
|
CodeMirror.runMode(content, syntax.mime, el, {
|
||||||
tabSize: indentSize
|
tabSize: indentSize
|
||||||
})
|
})
|
||||||
@@ -782,6 +854,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
canvas.height = height.value + 'vh'
|
canvas.height = height.value + 'vh'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const chart = new Chart(canvas, chartConfig)
|
const chart = new Chart(canvas, chartConfig)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
el.className = 'chart-error'
|
el.className = 'chart-error'
|
||||||
@@ -792,9 +865,134 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
||||||
el => {
|
el => {
|
||||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
|
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme, mermaidHTMLLabel)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_.forEach(
|
||||||
|
this.refs.root.contentWindow.document.querySelectorAll('.gallery'),
|
||||||
|
el => {
|
||||||
|
const images = el.innerHTML.split(/\n/g).filter(i => i.length > 0)
|
||||||
|
el.innerHTML = ''
|
||||||
|
|
||||||
|
const height = el.attributes.getNamedItem('data-height')
|
||||||
|
if (height && height.value !== 'undefined') {
|
||||||
|
el.style.height = height.value + 'vh'
|
||||||
|
}
|
||||||
|
|
||||||
|
let autoplay = el.attributes.getNamedItem('data-autoplay')
|
||||||
|
if (autoplay && autoplay.value !== 'undefined') {
|
||||||
|
autoplay = parseInt(autoplay.value, 10) || 0
|
||||||
|
} else {
|
||||||
|
autoplay = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Carousel
|
||||||
|
images={images}
|
||||||
|
autoplay={autoplay}
|
||||||
|
/>,
|
||||||
|
el
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
|
||||||
|
const rect = markdownPreviewIframe.getBoundingClientRect()
|
||||||
|
const config = { attributes: true, subtree: true }
|
||||||
|
const imgObserver = new MutationObserver((mutationList) => {
|
||||||
|
for (const mu of mutationList) {
|
||||||
|
if (mu.target.className === 'carouselContent-enter-done') {
|
||||||
|
this.setImgOnClickEventHelper(mu.target, rect)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
|
||||||
|
for (const img of imgList) {
|
||||||
|
const parentEl = img.parentElement
|
||||||
|
this.setImgOnClickEventHelper(img, rect)
|
||||||
|
imgObserver.observe(parentEl, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('a')
|
||||||
|
for (const a of aList) {
|
||||||
|
a.removeEventListener('click', this.linkClickHandler)
|
||||||
|
a.addEventListener('click', this.linkClickHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setImgOnClickEventHelper (img, rect) {
|
||||||
|
img.onclick = () => {
|
||||||
|
const widthMagnification = document.body.clientWidth / img.width
|
||||||
|
const heightMagnification = document.body.clientHeight / img.height
|
||||||
|
const baseOnWidth = widthMagnification < heightMagnification
|
||||||
|
const magnification = baseOnWidth ? widthMagnification : heightMagnification
|
||||||
|
|
||||||
|
const zoomImgWidth = img.width * magnification
|
||||||
|
const zoomImgHeight = img.height * magnification
|
||||||
|
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
|
||||||
|
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
|
||||||
|
const originalImgTop = img.y + rect.top
|
||||||
|
const originalImgLeft = img.x + rect.left
|
||||||
|
const originalImgRect = {
|
||||||
|
top: `${originalImgTop}px`,
|
||||||
|
left: `${originalImgLeft}px`,
|
||||||
|
width: `${img.width}px`,
|
||||||
|
height: `${img.height}px`
|
||||||
|
}
|
||||||
|
const zoomInImgRect = {
|
||||||
|
top: `${baseOnWidth ? zoomImgTop : 0}px`,
|
||||||
|
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
|
||||||
|
width: `${zoomImgWidth}px`,
|
||||||
|
height: `${zoomImgHeight}px`
|
||||||
|
}
|
||||||
|
const animationSpeed = 300
|
||||||
|
|
||||||
|
const zoomImg = document.createElement('img')
|
||||||
|
zoomImg.src = img.src
|
||||||
|
zoomImg.style = `
|
||||||
|
position: absolute;
|
||||||
|
top: ${baseOnWidth ? zoomImgTop : 0}px;
|
||||||
|
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
|
||||||
|
width: ${zoomImgWidth};
|
||||||
|
height: ${zoomImgHeight}px;
|
||||||
|
`
|
||||||
|
zoomImg.animate([
|
||||||
|
originalImgRect,
|
||||||
|
zoomInImgRect
|
||||||
|
], animationSpeed)
|
||||||
|
|
||||||
|
const overlay = document.createElement('div')
|
||||||
|
overlay.style = `
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
cursor: zoom-out;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: ${document.body.clientHeight}px;
|
||||||
|
z-index: 100;
|
||||||
|
`
|
||||||
|
overlay.onclick = () => {
|
||||||
|
zoomImg.style = `
|
||||||
|
position: absolute;
|
||||||
|
top: ${originalImgTop}px;
|
||||||
|
left: ${originalImgLeft}px;
|
||||||
|
width: ${img.width}px;
|
||||||
|
height: ${img.height}px;
|
||||||
|
`
|
||||||
|
const zoomOutImgAnimation = zoomImg.animate([
|
||||||
|
zoomInImgRect,
|
||||||
|
originalImgRect
|
||||||
|
], animationSpeed)
|
||||||
|
zoomOutImgAnimation.onfinish = () => overlay.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.appendChild(zoomImg)
|
||||||
|
document.body.appendChild(overlay)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
@@ -805,7 +1003,11 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
return this.refs.root.contentWindow
|
return this.refs.root.contentWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollTo (targetRow) {
|
/**
|
||||||
|
* @public
|
||||||
|
* @param {Number} targetRow
|
||||||
|
*/
|
||||||
|
scrollToRow (targetRow) {
|
||||||
const blocks = this.getWindow().document.querySelectorAll(
|
const blocks = this.getWindow().document.querySelectorAll(
|
||||||
'body>[data-line]'
|
'body>[data-line]'
|
||||||
)
|
)
|
||||||
@@ -815,12 +1017,21 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
const row = parseInt(block.getAttribute('data-line'))
|
const row = parseInt(block.getAttribute('data-line'))
|
||||||
if (row > targetRow || index === blocks.length - 1) {
|
if (row > targetRow || index === blocks.length - 1) {
|
||||||
block = blocks[index - 1]
|
block = blocks[index - 1]
|
||||||
block != null && this.getWindow().scrollTo(0, block.offsetTop)
|
block != null && this.scrollTo(0, block.offsetTop)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `document.body.scrollTo`
|
||||||
|
* @param {Number} x
|
||||||
|
* @param {Number} y
|
||||||
|
*/
|
||||||
|
scrollTo (x, y) {
|
||||||
|
this.getWindow().document.body.scrollTo(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
preventImageDroppedHandler (e) {
|
preventImageDroppedHandler (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -837,24 +1048,36 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
return new window.Notification(title, options)
|
return new window.Notification(title, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
handlelinkClick (e) {
|
handleLinkClick (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
const href = e.target.href
|
const rawHref = e.target.getAttribute('href')
|
||||||
const linkHash = href.split('/').pop()
|
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
|
||||||
|
|
||||||
const regexNoteInternalLink = /main.html#(.+)/
|
const parser = document.createElement('a')
|
||||||
if (regexNoteInternalLink.test(linkHash)) {
|
parser.href = rawHref
|
||||||
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
|
const isStartWithHash = rawHref[0] === '#'
|
||||||
const targetElement = this.refs.root.contentWindow.document.getElementById(
|
const { href, hash } = parser
|
||||||
targetId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (targetElement != null) {
|
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
|
||||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
|
||||||
|
const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html
|
||||||
|
const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`)
|
||||||
|
if (isStartWithHash || regexNoteInternalLink.test(rawHref)) {
|
||||||
|
const posOfHash = linkHash.indexOf('#')
|
||||||
|
if (posOfHash > -1) {
|
||||||
|
const extractedId = linkHash.slice(posOfHash + 1)
|
||||||
|
const targetId = mdurl.encode(extractedId)
|
||||||
|
const targetElement = this.getWindow().document.getElementById(
|
||||||
|
targetId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (targetElement != null) {
|
||||||
|
this.scrollTo(0, targetElement.offsetTop)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this will match the new uuid v4 hash and the old hash
|
// this will match the new uuid v4 hash and the old hash
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
this.refs.code.setValue(value)
|
this.refs.code.setValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOnChange () {
|
handleOnChange (e) {
|
||||||
this.value = this.refs.code.value
|
this.value = this.refs.code.value
|
||||||
this.props.onChange()
|
this.props.onChange(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleScroll (e) {
|
handleScroll (e) {
|
||||||
@@ -78,22 +78,25 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const idMatch = /checkbox-([0-9]+)/
|
const idMatch = /checkbox-([0-9]+)/
|
||||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
|
||||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
|
||||||
|
const checkReplace = /\[x]/i
|
||||||
|
const uncheckReplace = /\[ ]/
|
||||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
const lines = this.refs.code.value
|
const lines = this.refs.code.value
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|
||||||
const targetLine = lines[lineIndex]
|
const targetLine = lines[lineIndex]
|
||||||
|
let newLine = targetLine
|
||||||
|
|
||||||
if (targetLine.match(checkedMatch)) {
|
if (targetLine.match(checkedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(checkedMatch, '- [ ]')
|
newLine = targetLine.replace(checkReplace, '[ ]')
|
||||||
}
|
}
|
||||||
if (targetLine.match(uncheckedMatch)) {
|
if (targetLine.match(uncheckedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '- [x]')
|
newLine = targetLine.replace(uncheckReplace, '[x]')
|
||||||
}
|
}
|
||||||
this.refs.code.setValue(lines.join('\n'))
|
this.refs.code.setLineContent(lineIndex, newLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +137,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {config, value, storageKey, noteKey} = this.props
|
const {config, value, storageKey, noteKey, linesHighlighted} = this.props
|
||||||
const storage = findStorage(storageKey)
|
const storage = findStorage(storageKey)
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
@@ -148,7 +151,6 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
onMouseMove={e => this.handleMouseMove(e)}
|
onMouseMove={e => this.handleMouseMove(e)}
|
||||||
onMouseUp={e => this.handleMouseUp(e)}>
|
onMouseUp={e => this.handleMouseUp(e)}>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
styleName='codeEditor'
|
|
||||||
ref='code'
|
ref='code'
|
||||||
width={this.state.codeEditorWidthInPercent + '%'}
|
width={this.state.codeEditorWidthInPercent + '%'}
|
||||||
mode='Boost Flavored Markdown'
|
mode='Boost Flavored Markdown'
|
||||||
@@ -158,6 +160,10 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
fontFamily={config.editor.fontFamily}
|
fontFamily={config.editor.fontFamily}
|
||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
lineWrapping
|
||||||
|
matchingPairs={config.editor.matchingPairs}
|
||||||
|
matchingTriples={config.editor.matchingTriples}
|
||||||
|
explodingPairs={config.editor.explodingPairs}
|
||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
enableRulers={config.editor.enableRulers}
|
enableRulers={config.editor.enableRulers}
|
||||||
@@ -167,15 +173,22 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
enableTableEditor={config.editor.enableTableEditor}
|
enableTableEditor={config.editor.enableTableEditor}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
onChange={this.handleOnChange.bind(this)}
|
linesHighlighted={linesHighlighted}
|
||||||
|
onChange={(e) => this.handleOnChange(e)}
|
||||||
onScroll={this.handleScroll.bind(this)}
|
onScroll={this.handleScroll.bind(this)}
|
||||||
|
spellCheck={config.editor.spellcheck}
|
||||||
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
|
hotkey={config.hotkey}
|
||||||
|
switchPreview={config.editor.switchPreview}
|
||||||
|
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||||
|
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||||
|
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||||
/>
|
/>
|
||||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||||
<div styleName='slider-hitbox' />
|
<div styleName='slider-hitbox' />
|
||||||
</div>
|
</div>
|
||||||
<MarkdownPreview
|
<MarkdownPreview
|
||||||
style={previewStyle}
|
style={previewStyle}
|
||||||
styleName='preview'
|
|
||||||
theme={config.ui.theme}
|
theme={config.ui.theme}
|
||||||
keyMap={config.editor.keyMap}
|
keyMap={config.editor.keyMap}
|
||||||
fontSize={config.preview.fontSize}
|
fontSize={config.preview.fontSize}
|
||||||
@@ -188,6 +201,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
smartArrows={config.preview.smartArrows}
|
smartArrows={config.preview.smartArrows}
|
||||||
breaks={config.preview.breaks}
|
breaks={config.preview.breaks}
|
||||||
sanitize={config.preview.sanitize}
|
sanitize={config.preview.sanitize}
|
||||||
|
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
tabInde='0'
|
tabInde='0'
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -8,9 +8,30 @@
|
|||||||
top -2px
|
top -2px
|
||||||
width 0
|
width 0
|
||||||
z-index 0
|
z-index 0
|
||||||
|
border-left 1px solid $ui-borderColor
|
||||||
.slider-hitbox
|
.slider-hitbox
|
||||||
absolute top bottom left right
|
absolute top bottom left right
|
||||||
width 7px
|
width 7px
|
||||||
left -3px
|
left -3px
|
||||||
z-index 10
|
z-index 10
|
||||||
cursor col-resize
|
cursor col-resize
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
.slider
|
||||||
|
border-left 1px solid $ui-dark-borderColor
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
.slider
|
||||||
|
border-left 1px solid $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.root
|
||||||
|
.slider
|
||||||
|
border-left 1px solid $ui-monokai-borderColor
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
.slider
|
||||||
|
border-left 1px solid $ui-dracula-borderColor
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const ModalEscButton = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<button styleName='escButton' onClick={handleEscButtonClick}>
|
<button styleName='escButton' onClick={handleEscButtonClick}>
|
||||||
<div styleName='esc-mark'>×</div>
|
<div styleName='esc-mark'>×</div>
|
||||||
<div styleName='esc-text'>esc</div>
|
<div>esc</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const NavToggleButton = ({isFolded, handleToggleButtonClick}) => (
|
|||||||
onClick={(e) => handleToggleButtonClick(e)}
|
onClick={(e) => handleToggleButtonClick(e)}
|
||||||
>
|
>
|
||||||
{isFolded
|
{isFolded
|
||||||
? <i className='fa fa-angle-double-right' />
|
? <i className='fa fa-angle-double-right fa-2x' />
|
||||||
: <i className='fa fa-angle-double-left' />
|
: <i className='fa fa-angle-double-left fa-2x' />
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
border-radius 16.5px
|
border-radius 16.5px
|
||||||
height 34px
|
height 34px
|
||||||
width 34px
|
width 34px
|
||||||
line-height 32px
|
line-height 100%
|
||||||
padding 0
|
padding 0
|
||||||
&:hover
|
&:hover
|
||||||
border: 1px solid #1EC38B;
|
border: 1px solid #1EC38B;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { isArray } from 'lodash'
|
import { isArray, sortBy } from 'lodash'
|
||||||
|
import invertColor from 'invert-color'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
||||||
import styles from './NoteItem.styl'
|
import styles from './NoteItem.styl'
|
||||||
@@ -13,29 +14,38 @@ import i18n from 'browser/lib/i18n'
|
|||||||
/**
|
/**
|
||||||
* @description Tag element component.
|
* @description Tag element component.
|
||||||
* @param {string} tagName
|
* @param {string} tagName
|
||||||
|
* @param {string} color
|
||||||
* @return {React.Component}
|
* @return {React.Component}
|
||||||
*/
|
*/
|
||||||
const TagElement = ({ tagName }) => (
|
const TagElement = ({ tagName, color }) => {
|
||||||
<span styleName='item-bottom-tagList-item' key={tagName}>
|
const style = {}
|
||||||
#{tagName}
|
if (color) {
|
||||||
</span>
|
style.backgroundColor = color
|
||||||
)
|
style.color = invertColor(color, { black: '#222', white: '#f1f1f1', threshold: 0.3 })
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span styleName='item-bottom-tagList-item' key={tagName} style={style}>
|
||||||
|
#{tagName}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Tag element list component.
|
* @description Tag element list component.
|
||||||
* @param {Array|null} tags
|
* @param {Array|null} tags
|
||||||
* @param {boolean} showTagsAlphabetically
|
* @param {boolean} showTagsAlphabetically
|
||||||
|
* @param {Object} coloredTags
|
||||||
* @return {React.Component}
|
* @return {React.Component}
|
||||||
*/
|
*/
|
||||||
const TagElementList = (tags, showTagsAlphabetically) => {
|
const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
|
||||||
if (!isArray(tags)) {
|
if (!isArray(tags)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showTagsAlphabetically) {
|
if (showTagsAlphabetically) {
|
||||||
return _.sortBy(tags).map(tag => TagElement({ tagName: tag }))
|
return sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||||
} else {
|
} else {
|
||||||
return tags.map(tag => TagElement({ tagName: tag }))
|
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +56,7 @@ const TagElementList = (tags, showTagsAlphabetically) => {
|
|||||||
* @param {Function} handleNoteClick
|
* @param {Function} handleNoteClick
|
||||||
* @param {Function} handleNoteContextMenu
|
* @param {Function} handleNoteContextMenu
|
||||||
* @param {Function} handleDragStart
|
* @param {Function} handleDragStart
|
||||||
|
* @param {Object} coloredTags
|
||||||
* @param {string} dateDisplay
|
* @param {string} dateDisplay
|
||||||
*/
|
*/
|
||||||
const NoteItem = ({
|
const NoteItem = ({
|
||||||
@@ -59,7 +70,8 @@ const NoteItem = ({
|
|||||||
storageName,
|
storageName,
|
||||||
folderName,
|
folderName,
|
||||||
viewType,
|
viewType,
|
||||||
showTagsAlphabetically
|
showTagsAlphabetically,
|
||||||
|
coloredTags
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
styleName={isActive ? 'item--active' : 'item'}
|
styleName={isActive ? 'item--active' : 'item'}
|
||||||
@@ -97,7 +109,7 @@ const NoteItem = ({
|
|||||||
<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, showTagsAlphabetically)
|
? TagElementList(note.tags, showTagsAlphabetically, coloredTags)
|
||||||
: <span
|
: <span
|
||||||
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
||||||
styleName='item-bottom-tagList-empty'
|
styleName='item-bottom-tagList-empty'
|
||||||
@@ -127,6 +139,7 @@ const NoteItem = ({
|
|||||||
NoteItem.propTypes = {
|
NoteItem.propTypes = {
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
dateDisplay: PropTypes.string.isRequired,
|
dateDisplay: PropTypes.string.isRequired,
|
||||||
|
coloredTags: PropTypes.object,
|
||||||
note: PropTypes.shape({
|
note: PropTypes.shape({
|
||||||
storage: PropTypes.string.isRequired,
|
storage: PropTypes.string.isRequired,
|
||||||
key: PropTypes.string.isRequired,
|
key: PropTypes.string.isRequired,
|
||||||
@@ -135,15 +148,14 @@ NoteItem.propTypes = {
|
|||||||
tags: PropTypes.array,
|
tags: PropTypes.array,
|
||||||
isStarred: PropTypes.bool.isRequired,
|
isStarred: PropTypes.bool.isRequired,
|
||||||
isTrashed: PropTypes.bool.isRequired,
|
isTrashed: PropTypes.bool.isRequired,
|
||||||
blog: {
|
blog: PropTypes.shape({
|
||||||
blogLink: PropTypes.string,
|
blogLink: PropTypes.string,
|
||||||
blogId: PropTypes.number
|
blogId: PropTypes.number
|
||||||
}
|
})
|
||||||
}),
|
}),
|
||||||
handleNoteClick: PropTypes.func.isRequired,
|
handleNoteClick: PropTypes.func.isRequired,
|
||||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||||
handleDragStart: PropTypes.func.isRequired,
|
handleDragStart: PropTypes.func.isRequired
|
||||||
handleDragEnd: PropTypes.func.isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(NoteItem, styles)
|
export default CSSModules(NoteItem, styles)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ SideNavFilter.propTypes = {
|
|||||||
isStarredActive: PropTypes.bool.isRequired,
|
isStarredActive: PropTypes.bool.isRequired,
|
||||||
isTrashedActive: PropTypes.bool.isRequired,
|
isTrashedActive: PropTypes.bool.isRequired,
|
||||||
handleStarredButtonClick: PropTypes.func.isRequired,
|
handleStarredButtonClick: PropTypes.func.isRequired,
|
||||||
handleTrashdButtonClick: PropTypes.func.isRequired
|
handleTrashedButtonClick: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(SideNavFilter, styles)
|
export default CSSModules(SideNavFilter, styles)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class SnippetTab extends React.Component {
|
|||||||
>
|
>
|
||||||
{snippet.name.trim().length > 0
|
{snippet.name.trim().length > 0
|
||||||
? snippet.name
|
? snippet.name
|
||||||
: <span styleName='button-unnamed'>
|
: <span>
|
||||||
{i18n.__('Unnamed')}
|
{i18n.__('Unnamed')}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,30 @@
|
|||||||
flex 1
|
flex 1
|
||||||
min-width 70px
|
min-width 70px
|
||||||
overflow hidden
|
overflow hidden
|
||||||
|
border-left 1px solid $ui-borderColor
|
||||||
|
border-top 1px solid $ui-borderColor
|
||||||
&:hover
|
&:hover
|
||||||
|
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||||
.deleteButton
|
.deleteButton
|
||||||
color $ui-inactive-text-color
|
color: $ui-text-color
|
||||||
&:hover
|
visibility visible
|
||||||
background-color darken($ui-backgroundColor, 15%)
|
transition 0.15s
|
||||||
&:active
|
.button
|
||||||
color white
|
color: $ui-text-color
|
||||||
background-color $ui-active-color
|
transition 0.15s
|
||||||
|
|
||||||
.root--active
|
.root--active
|
||||||
@extend .root
|
@extend .root
|
||||||
min-width 100px
|
min-width 100px
|
||||||
border-bottom $ui-border
|
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||||
|
.deleteButton
|
||||||
|
visibility visible
|
||||||
|
color: $ui-text-color
|
||||||
|
transition 0.15s
|
||||||
|
.button
|
||||||
|
font-weight bold
|
||||||
|
color: $ui-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
.button
|
.button
|
||||||
width 100%
|
width 100%
|
||||||
@@ -27,8 +38,7 @@
|
|||||||
background-color transparent
|
background-color transparent
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
border-left 4px solid transparent
|
border-left 4px solid transparent
|
||||||
&:hover
|
color $ui-inactive-text-color
|
||||||
background-color $ui-button--hover-backgroundColor
|
|
||||||
|
|
||||||
.deleteButton
|
.deleteButton
|
||||||
position absolute
|
position absolute
|
||||||
@@ -42,6 +52,7 @@
|
|||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
background-color transparent
|
background-color transparent
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
|
visibility hidden
|
||||||
|
|
||||||
.input
|
.input
|
||||||
height 29px
|
height 29px
|
||||||
@@ -50,76 +61,66 @@
|
|||||||
width 100%
|
width 100%
|
||||||
outline none
|
outline none
|
||||||
|
|
||||||
|
body[data-theme="default"], body[data-theme="white"]
|
||||||
|
.root--active
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
color $ui-dark-text-color
|
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
|
border-top 1px solid $ui-dark-borderColor
|
||||||
&:hover
|
&:hover
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
transition 0.15s
|
||||||
|
.button
|
||||||
|
color $ui-dark-text-color
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
.deleteButton
|
||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-text-color
|
||||||
&:hover
|
transition 0.15s
|
||||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
|
||||||
&:active
|
|
||||||
color $ui-dark-text-color
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
.root--active
|
.root--active
|
||||||
color $ui-dark-text-color
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
border-color $ui-dark-borderColor
|
border-left 1px solid $ui-dark-borderColor
|
||||||
&:hover
|
border-top 1px solid $ui-dark-borderColor
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
.button
|
||||||
.deleteButton
|
color $ui-dark-text-color
|
||||||
color $ui-dark-inactive-text-color
|
.deleteButton
|
||||||
&:hover
|
color $ui-dark-text-color
|
||||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
|
||||||
&:active
|
|
||||||
color $ui-dark-text-color
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
.button
|
.button
|
||||||
border none
|
border none
|
||||||
color $ui-dark-text-color
|
|
||||||
background-color transparent
|
background-color transparent
|
||||||
transition color background-color 0.15s
|
transition color background-color 0.15s
|
||||||
border-left 4px solid transparent
|
border-left 4px solid transparent
|
||||||
&:hover
|
|
||||||
color $ui-dark-text-color
|
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
|
||||||
|
|
||||||
.input
|
.input
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
|
||||||
color alpha($ui-dark-text-color, 30%)
|
|
||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
.root
|
.root
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
border-color $ui-dark-borderColor
|
|
||||||
&:hover
|
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
|
||||||
.deleteButton
|
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
&:hover
|
|
||||||
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
|
||||||
&:active
|
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
.root--active
|
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
border-color $ui-solarized-dark-borderColor
|
border-color $ui-solarized-dark-borderColor
|
||||||
&:hover
|
&:hover
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
.deleteButton
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-button--active-color
|
||||||
&:hover
|
transition 0.15s
|
||||||
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
.button
|
||||||
&:active
|
color $ui-solarized-dark-button--active-color
|
||||||
color $ui-solarized-dark-text-color
|
transition 0.15s
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
.root--active
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
.button
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
|
||||||
.button
|
.button
|
||||||
border none
|
border none
|
||||||
@@ -127,101 +128,75 @@ body[data-theme="solarized-dark"]
|
|||||||
background-color transparent
|
background-color transparent
|
||||||
transition color background-color 0.15s
|
transition color background-color 0.15s
|
||||||
border-left 4px solid transparent
|
border-left 4px solid transparent
|
||||||
&:hover
|
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.input
|
.input
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-button--active-color
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
|
||||||
color alpha($ui-solarized-dark-text-color, 30%)
|
|
||||||
|
|
||||||
body[data-theme="monokai"]
|
body[data-theme="monokai"]
|
||||||
.root
|
.root
|
||||||
color $ui-monokai-text-color
|
|
||||||
border-color $ui-dark-borderColor
|
|
||||||
&:hover
|
|
||||||
background-color $ui-monokai-noteDetail-backgroundColor
|
|
||||||
.deleteButton
|
|
||||||
color $ui-monokai-text-color
|
|
||||||
&:hover
|
|
||||||
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
|
|
||||||
&:active
|
|
||||||
color $ui-monokai-text-color
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
.root--active
|
|
||||||
color $ui-monokai-text-color
|
|
||||||
border-color $ui-monokai-borderColor
|
border-color $ui-monokai-borderColor
|
||||||
&:hover
|
&:hover
|
||||||
background-color $ui-monokai-noteDetail-backgroundColor
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
.deleteButton
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
&:hover
|
transition 0.15s
|
||||||
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
|
.button
|
||||||
&:active
|
color $ui-monokai-text-color
|
||||||
color $ui-monokai-text-color
|
transition 0.15s
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
color $ui-monokai-active-color
|
||||||
|
background-color $ui-monokai-button-backgroundColor
|
||||||
|
border-color $ui-monokai-borderColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
.button
|
||||||
|
color $ui-monokai-active-color
|
||||||
|
|
||||||
.button
|
.button
|
||||||
border none
|
border none
|
||||||
color $ui-monokai-text-color
|
color $ui-inactive-text-color
|
||||||
background-color transparent
|
background-color transparent
|
||||||
transition color background-color 0.15s
|
transition color background-color 0.15s
|
||||||
border-left 4px solid transparent
|
border-left 4px solid transparent
|
||||||
&:hover
|
|
||||||
color $ui-monokai-text-color
|
|
||||||
background-color $ui-monokai-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.input
|
.input
|
||||||
background-color $ui-monokai-noteDetail-backgroundColor
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
|
||||||
color alpha($ui-monokai-text-color, 30%)
|
|
||||||
|
|
||||||
body[data-theme="dracula"]
|
body[data-theme="dracula"]
|
||||||
.root
|
.root
|
||||||
color $ui-dracula-text-color
|
|
||||||
border-color $ui-dark-borderColor
|
|
||||||
&:hover
|
|
||||||
background-color $ui-dracula-noteDetail-backgroundColor
|
|
||||||
.deleteButton
|
|
||||||
color $ui-dracula-text-color
|
|
||||||
&:hover
|
|
||||||
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
|
|
||||||
&:active
|
|
||||||
color $ui-dracula-text-color
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
.root--active
|
|
||||||
color $ui-dracula-text-color
|
|
||||||
border-color $ui-dracula-borderColor
|
border-color $ui-dracula-borderColor
|
||||||
&:hover
|
&:hover
|
||||||
background-color $ui-dracula-noteDetail-backgroundColor
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
.deleteButton
|
||||||
color $ui-dracula-text-color
|
color $ui-dracula-text-color
|
||||||
&:hover
|
transition 0.15s
|
||||||
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
|
.button
|
||||||
&:active
|
color $ui-dracula-text-color
|
||||||
color $ui-dracula-text-color
|
transition 0.15s
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
.root--active
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.button
|
||||||
|
color $ui-dracula-active-color
|
||||||
|
|
||||||
.button
|
.button
|
||||||
border none
|
border none
|
||||||
color $ui-dracula-text-color
|
color $ui-inactive-text-color
|
||||||
background-color transparent
|
background-color transparent
|
||||||
transition color background-color 0.15s
|
transition color background-color 0.15s
|
||||||
border-left 4px solid transparent
|
border-left 4px solid transparent
|
||||||
&:hover
|
|
||||||
color $ui-dracula-text-color
|
|
||||||
background-color $ui-dracula-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.input
|
.input
|
||||||
background-color $ui-dracula-noteDetail-backgroundColor
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
color $ui-dracula-text-color
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
.deleteButton
|
|
||||||
color alpha($ui-dracula-text-color, 30%)
|
|
||||||
@@ -10,11 +10,12 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {Function} handleClickTagListItem
|
* @param {Function} handleClickTagListItem
|
||||||
* @param {Function} handleClickNarrowToTag
|
* @param {Function} handleClickNarrowToTag
|
||||||
* @param {bool} isActive
|
* @param {boolean} isActive
|
||||||
* @param {bool} isRelated
|
* @param {boolean} isRelated
|
||||||
|
* @param {string} bgColor tab backgroundColor
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => (
|
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count, color}) => (
|
||||||
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}>
|
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}>
|
||||||
{isRelated
|
{isRelated
|
||||||
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
||||||
@@ -23,6 +24,7 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, hand
|
|||||||
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
||||||
}
|
}
|
||||||
<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-color' style={{backgroundColor: color || 'transparent'}} />
|
||||||
<span styleName='tagList-item-name'>
|
<span styleName='tagList-item-name'>
|
||||||
{`# ${name}`}
|
{`# ${name}`}
|
||||||
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
|
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
|
||||||
@@ -33,7 +35,8 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, hand
|
|||||||
|
|
||||||
TagListItem.propTypes = {
|
TagListItem.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
handleClickTagListItem: PropTypes.func.isRequired
|
handleClickTagListItem: PropTypes.func.isRequired,
|
||||||
|
color: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(TagListItem, styles)
|
export default CSSModules(TagListItem, styles)
|
||||||
|
|||||||
@@ -71,6 +71,11 @@
|
|||||||
padding-right 15px
|
padding-right 15px
|
||||||
font-size 13px
|
font-size 13px
|
||||||
|
|
||||||
|
.tagList-item-color
|
||||||
|
height 26px
|
||||||
|
width 3px
|
||||||
|
display inline-block
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.tagList-item
|
.tagList-item
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ const TodoProcess = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
TodoProcess.propTypes = {
|
TodoProcess.propTypes = {
|
||||||
todoStatus: {
|
todoStatus: PropTypes.exact({
|
||||||
total: PropTypes.number.isRequired,
|
total: PropTypes.number.isRequired,
|
||||||
completed: PropTypes.number.isRequired
|
completed: PropTypes.number.isRequired
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(TodoProcess, styles)
|
export default CSSModules(TodoProcess, styles)
|
||||||
|
|||||||
@@ -55,11 +55,14 @@ body
|
|||||||
line-height 1.6
|
line-height 1.6
|
||||||
overflow-x hidden
|
overflow-x hidden
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
|
// do not allow display line breaks
|
||||||
|
.katex-display > .katex
|
||||||
|
white-space nowrap
|
||||||
|
// allow inline line breaks
|
||||||
.katex
|
.katex
|
||||||
font 400 1.2em 'KaTeX_Main'
|
|
||||||
line-height 1.2em
|
|
||||||
white-space initial
|
white-space initial
|
||||||
text-indent 0
|
.katex .katex-html
|
||||||
|
display inline-flex
|
||||||
.katex .mfrac>.vlist>span:nth-child(2)
|
.katex .mfrac>.vlist>span:nth-child(2)
|
||||||
top 0 !important
|
top 0 !important
|
||||||
.katex-error
|
.katex-error
|
||||||
@@ -162,6 +165,7 @@ p
|
|||||||
white-space pre-line
|
white-space pre-line
|
||||||
word-wrap break-word
|
word-wrap break-word
|
||||||
img
|
img
|
||||||
|
cursor zoom-in
|
||||||
max-width 100%
|
max-width 100%
|
||||||
strong, b
|
strong, b
|
||||||
font-weight bold
|
font-weight bold
|
||||||
@@ -183,6 +187,10 @@ ul
|
|||||||
display list-item
|
display list-item
|
||||||
&.taskListItem
|
&.taskListItem
|
||||||
list-style none
|
list-style none
|
||||||
|
&>input
|
||||||
|
margin-left -1.6em
|
||||||
|
&>p
|
||||||
|
margin-left -1.8em
|
||||||
p
|
p
|
||||||
margin 0
|
margin 0
|
||||||
&>li>ul, &>li>ol
|
&>li>ul, &>li>ol
|
||||||
@@ -355,7 +363,10 @@ admonition_types = {
|
|||||||
danger: {color: #c2185b, icon: "block"},
|
danger: {color: #c2185b, icon: "block"},
|
||||||
caution: {color: #ffa726, icon: "warning"},
|
caution: {color: #ffa726, icon: "warning"},
|
||||||
error: {color: #d32f2f, icon: "error_outline"},
|
error: {color: #d32f2f, icon: "error_outline"},
|
||||||
attention: {color: #455a64, icon: "priority_high"}
|
question: {color: #64dd17, icon: "help_outline"},
|
||||||
|
quote: {color: #9e9e9e, icon: "format_quote"},
|
||||||
|
abstract: {color: #00b0ff, icon: "subject"},
|
||||||
|
attention: {color: #455a64, icon: "priority_high"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, val in admonition_types
|
for name, val in admonition_types
|
||||||
@@ -416,6 +427,26 @@ pre.fence
|
|||||||
canvas, svg
|
canvas, svg
|
||||||
max-width 100% !important
|
max-width 100% !important
|
||||||
|
|
||||||
|
.gallery
|
||||||
|
width 100%
|
||||||
|
height 50vh
|
||||||
|
|
||||||
|
.carousel
|
||||||
|
.carousel-main img
|
||||||
|
min-width auto
|
||||||
|
max-width 100%
|
||||||
|
min-height auto
|
||||||
|
max-height 100%
|
||||||
|
|
||||||
|
.carousel-footer::-webkit-scrollbar-corner
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-text-color
|
||||||
|
background-color $ui-tag-backgroundColor
|
||||||
|
|
||||||
themeDarkBackground = darken(#21252B, 10%)
|
themeDarkBackground = darken(#21252B, 10%)
|
||||||
themeDarkText = #f9f9f9
|
themeDarkText = #f9f9f9
|
||||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||||
@@ -475,6 +506,14 @@ body[data-theme="dark"]
|
|||||||
border-color themeDarkBorder
|
border-color themeDarkBorder
|
||||||
background-color themeDarkPreview
|
background-color themeDarkPreview
|
||||||
|
|
||||||
|
pre.fence
|
||||||
|
.gallery
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-dark-text-color
|
||||||
|
background-color $ui-dark-tag-backgroundColor
|
||||||
|
|
||||||
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||||
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
|
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
|
||||||
@@ -510,6 +549,14 @@ body[data-theme="solarized-dark"]
|
|||||||
border-color themeDarkBorder
|
border-color themeDarkBorder
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
pre.fence
|
||||||
|
.gallery
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
|
||||||
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
||||||
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
||||||
themeMonokaiTableHead = themeMonokaiTableEven
|
themeMonokaiTableHead = themeMonokaiTableEven
|
||||||
@@ -538,6 +585,7 @@ body[data-theme="monokai"]
|
|||||||
border-right solid 1px themeMonokaiTableBorder
|
border-right solid 1px themeMonokaiTableBorder
|
||||||
kbd
|
kbd
|
||||||
background-color themeDarkBackground
|
background-color themeDarkBackground
|
||||||
|
|
||||||
dl
|
dl
|
||||||
border-color themeDarkBorder
|
border-color themeDarkBorder
|
||||||
background-color themeMonokaiTableHead
|
background-color themeMonokaiTableHead
|
||||||
@@ -547,6 +595,14 @@ body[data-theme="monokai"]
|
|||||||
border-color themeDarkBorder
|
border-color themeDarkBorder
|
||||||
background-color $ui-monokai-noteDetail-backgroundColor
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
pre.fence
|
||||||
|
.gallery
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-monokai-button--active-color
|
||||||
|
background-color $ui-monokai-button-backgroundColor
|
||||||
|
|
||||||
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
|
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
|
||||||
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
|
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
|
||||||
themeDraculaTableHead = themeDraculaTableEven
|
themeDraculaTableHead = themeDraculaTableEven
|
||||||
@@ -575,6 +631,7 @@ body[data-theme="dracula"]
|
|||||||
border-right solid 1px themeDraculaTableBorder
|
border-right solid 1px themeDraculaTableBorder
|
||||||
kbd
|
kbd
|
||||||
background-color themeDarkBackground
|
background-color themeDarkBackground
|
||||||
|
|
||||||
dl
|
dl
|
||||||
border-color themeDarkBorder
|
border-color themeDarkBorder
|
||||||
background-color themeDraculaTableHead
|
background-color themeDraculaTableHead
|
||||||
@@ -583,3 +640,11 @@ body[data-theme="dracula"]
|
|||||||
dd
|
dd
|
||||||
border-color themeDarkBorder
|
border-color themeDarkBorder
|
||||||
background-color $ui-dracula-noteDetail-backgroundColor
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
pre.fence
|
||||||
|
.gallery
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-dracula-button--active-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
|||||||
@@ -19,17 +19,18 @@ function getId () {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
function render (element, content, theme) {
|
function render (element, content, theme, enableHTMLLabel) {
|
||||||
try {
|
try {
|
||||||
const height = element.attributes.getNamedItem('data-height')
|
const height = element.attributes.getNamedItem('data-height')
|
||||||
if (height && height.value !== 'undefined') {
|
if (height && height.value !== 'undefined') {
|
||||||
element.style.height = height.value + 'vh'
|
element.style.height = height.value + 'vh'
|
||||||
}
|
}
|
||||||
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
|
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
|
||||||
mermaidAPI.initialize({
|
mermaidAPI.initialize({
|
||||||
theme: isDarkTheme ? 'dark' : 'default',
|
theme: isDarkTheme ? 'dark' : 'default',
|
||||||
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
||||||
useMaxWidth: false
|
useMaxWidth: false,
|
||||||
|
flowchart: { htmlLabels: enableHTMLLabel }
|
||||||
})
|
})
|
||||||
mermaidAPI.render(getId(), content, (svgGraph) => {
|
mermaidAPI.render(getId(), content, (svgGraph) => {
|
||||||
element.innerHTML = svgGraph
|
element.innerHTML = svgGraph
|
||||||
|
|||||||
78
browser/lib/CMLanguageList.js
Normal file
78
browser/lib/CMLanguageList.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
export const languageMaps = {
|
||||||
|
brainfuck: 'Brainfuck',
|
||||||
|
cpp: 'C++',
|
||||||
|
cs: 'C#',
|
||||||
|
clojure: 'Clojure',
|
||||||
|
'clojure-repl': 'ClojureScript',
|
||||||
|
cmake: 'CMake',
|
||||||
|
coffeescript: 'CoffeeScript',
|
||||||
|
crystal: 'Crystal',
|
||||||
|
css: 'CSS',
|
||||||
|
d: 'D',
|
||||||
|
dart: 'Dart',
|
||||||
|
delphi: 'Pascal',
|
||||||
|
diff: 'Diff',
|
||||||
|
django: 'Django',
|
||||||
|
dockerfile: 'Dockerfile',
|
||||||
|
ebnf: 'EBNF',
|
||||||
|
elm: 'Elm',
|
||||||
|
erlang: 'Erlang',
|
||||||
|
'erlang-repl': 'Erlang',
|
||||||
|
fortran: 'Fortran',
|
||||||
|
fsharp: 'F#',
|
||||||
|
gherkin: 'Gherkin',
|
||||||
|
go: 'Go',
|
||||||
|
groovy: 'Groovy',
|
||||||
|
haml: 'HAML',
|
||||||
|
haskell: 'Haskell',
|
||||||
|
haxe: 'Haxe',
|
||||||
|
http: 'HTTP',
|
||||||
|
ini: 'toml',
|
||||||
|
java: 'Java',
|
||||||
|
javascript: 'JavaScript',
|
||||||
|
json: 'JSON',
|
||||||
|
julia: 'Julia',
|
||||||
|
kotlin: 'Kotlin',
|
||||||
|
less: 'LESS',
|
||||||
|
livescript: 'LiveScript',
|
||||||
|
lua: 'Lua',
|
||||||
|
markdown: 'Markdown',
|
||||||
|
mathematica: 'Mathematica',
|
||||||
|
nginx: 'Nginx',
|
||||||
|
nsis: 'NSIS',
|
||||||
|
objectivec: 'Objective-C',
|
||||||
|
ocaml: 'Ocaml',
|
||||||
|
perl: 'Perl',
|
||||||
|
php: 'PHP',
|
||||||
|
powershell: 'PowerShell',
|
||||||
|
properties: 'Properties files',
|
||||||
|
protobuf: 'ProtoBuf',
|
||||||
|
python: 'Python',
|
||||||
|
puppet: 'Puppet',
|
||||||
|
q: 'Q',
|
||||||
|
r: 'R',
|
||||||
|
ruby: 'Ruby',
|
||||||
|
rust: 'Rust',
|
||||||
|
sas: 'SAS',
|
||||||
|
scala: 'Scala',
|
||||||
|
scheme: 'Scheme',
|
||||||
|
scss: 'SCSS',
|
||||||
|
shell: 'Shell',
|
||||||
|
smalltalk: 'Smalltalk',
|
||||||
|
sml: 'SML',
|
||||||
|
sql: 'SQL',
|
||||||
|
stylus: 'Stylus',
|
||||||
|
swift: 'Swift',
|
||||||
|
tcl: 'Tcl',
|
||||||
|
tex: 'LaTex',
|
||||||
|
typescript: 'TypeScript',
|
||||||
|
twig: 'Twig',
|
||||||
|
vbnet: 'VB.NET',
|
||||||
|
vbscript: 'VBScript',
|
||||||
|
verilog: 'Verilog',
|
||||||
|
vhdl: 'VHDL',
|
||||||
|
xml: 'HTML',
|
||||||
|
xquery: 'XQuery',
|
||||||
|
yaml: 'YAML',
|
||||||
|
elixir: 'Elixir'
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import CSSModules from 'react-css-modules'
|
import CSSModules from 'react-css-modules'
|
||||||
|
|
||||||
export default function (component, styles) {
|
export default function (component, styles) {
|
||||||
return CSSModules(component, styles, {errorWhenNotFound: false})
|
return CSSModules(component, styles, {handleNotFoundStyleName: 'log'})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ const languages = [
|
|||||||
name: 'Chinese (zh-TW)',
|
name: 'Chinese (zh-TW)',
|
||||||
locale: 'zh-TW'
|
locale: 'zh-TW'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Czech',
|
||||||
|
locale: 'cs'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Danish',
|
name: 'Danish',
|
||||||
locale: 'da'
|
locale: 'da'
|
||||||
@@ -62,10 +66,12 @@ const languages = [
|
|||||||
{
|
{
|
||||||
name: 'Spanish',
|
name: 'Spanish',
|
||||||
locale: 'es-ES'
|
locale: 'es-ES'
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
name: 'Turkish',
|
name: 'Turkish',
|
||||||
locale: 'tr'
|
locale: 'tr'
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
name: 'Thai',
|
name: 'Thai',
|
||||||
locale: 'th'
|
locale: 'th'
|
||||||
}
|
}
|
||||||
@@ -82,4 +88,3 @@ module.exports = {
|
|||||||
return languages
|
return languages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
91
browser/lib/SnippetManager.js
Normal file
91
browser/lib/SnippetManager.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
import fs from 'fs'
|
||||||
|
import consts from './consts'
|
||||||
|
|
||||||
|
class SnippetManager {
|
||||||
|
constructor () {
|
||||||
|
this.defaultSnippet = [
|
||||||
|
{
|
||||||
|
id: crypto.randomBytes(16).toString('hex'),
|
||||||
|
name: 'Dummy text',
|
||||||
|
prefix: ['lorem', 'ipsum'],
|
||||||
|
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
this.snippets = []
|
||||||
|
this.expandSnippet = this.expandSnippet.bind(this)
|
||||||
|
this.init = this.init.bind(this)
|
||||||
|
this.assignSnippets = this.assignSnippets.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
init () {
|
||||||
|
if (fs.existsSync(consts.SNIPPET_FILE)) {
|
||||||
|
try {
|
||||||
|
this.snippets = JSON.parse(
|
||||||
|
fs.readFileSync(consts.SNIPPET_FILE, { encoding: 'UTF-8' })
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error while parsing snippet file')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fs.writeFileSync(
|
||||||
|
consts.SNIPPET_FILE,
|
||||||
|
JSON.stringify(this.defaultSnippet, null, 4),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
this.snippets = this.defaultSnippet
|
||||||
|
}
|
||||||
|
|
||||||
|
assignSnippets (snippets) {
|
||||||
|
this.snippets = snippets
|
||||||
|
}
|
||||||
|
|
||||||
|
expandSnippet (wordBeforeCursor, cursor, cm) {
|
||||||
|
const templateCursorString = ':{}'
|
||||||
|
for (let i = 0; i < this.snippets.length; i++) {
|
||||||
|
if (this.snippets[i].prefix.indexOf(wordBeforeCursor.text) === -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (this.snippets[i].content.indexOf(templateCursorString) !== -1) {
|
||||||
|
const snippetLines = this.snippets[i].content.split('\n')
|
||||||
|
let cursorLineNumber = 0
|
||||||
|
let cursorLinePosition = 0
|
||||||
|
|
||||||
|
let cursorIndex
|
||||||
|
for (let j = 0; j < snippetLines.length; j++) {
|
||||||
|
cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||||
|
|
||||||
|
if (cursorIndex !== -1) {
|
||||||
|
cursorLineNumber = j
|
||||||
|
cursorLinePosition = cursorIndex
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.replaceRange(
|
||||||
|
this.snippets[i].content.replace(templateCursorString, ''),
|
||||||
|
wordBeforeCursor.range.from,
|
||||||
|
wordBeforeCursor.range.to
|
||||||
|
)
|
||||||
|
cm.setCursor({
|
||||||
|
line: cursor.line + cursorLineNumber,
|
||||||
|
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cm.replaceRange(
|
||||||
|
this.snippets[i].content,
|
||||||
|
wordBeforeCursor.range.from,
|
||||||
|
wordBeforeCursor.range.to
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = new SnippetManager()
|
||||||
|
export default manager
|
||||||
@@ -6,7 +6,7 @@ const { dialog } = remote
|
|||||||
export function confirmDeleteNote (confirmDeletion, permanent) {
|
export function confirmDeleteNote (confirmDeletion, permanent) {
|
||||||
if (confirmDeletion || permanent) {
|
if (confirmDeletion || permanent) {
|
||||||
const alertConfig = {
|
const alertConfig = {
|
||||||
ype: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Confirm note deletion'),
|
message: i18n.__('Confirm note deletion'),
|
||||||
detail: i18n.__('This will permanently remove this note.'),
|
detail: i18n.__('This will permanently remove this note.'),
|
||||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
|
|||||||
@@ -3,14 +3,43 @@ const fs = require('sander')
|
|||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { app } = remote
|
const { app } = remote
|
||||||
|
|
||||||
const themePath = process.env.NODE_ENV === 'production'
|
const CODEMIRROR_THEME_PATH = 'node_modules/codemirror/theme'
|
||||||
? path.join(app.getAppPath(), './node_modules/codemirror/theme')
|
const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme'
|
||||||
: require('path').resolve('./node_modules/codemirror/theme')
|
|
||||||
const themes = fs.readdirSync(themePath)
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
.map((themePath) => {
|
|
||||||
return themePath.substring(0, themePath.lastIndexOf('.'))
|
const paths = [
|
||||||
})
|
isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH),
|
||||||
themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
|
isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH)
|
||||||
|
]
|
||||||
|
|
||||||
|
const themes = paths
|
||||||
|
.map(directory => fs.readdirSync(directory).map(file => {
|
||||||
|
const name = file.substring(0, file.lastIndexOf('.'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
path: path.join(directory, file),
|
||||||
|
className: `cm-s-${name}`
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.reduce((accumulator, value) => accumulator.concat(value), [])
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
|
themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, {
|
||||||
|
name: 'solarized dark',
|
||||||
|
path: path.join(paths[0], 'solarized.css'),
|
||||||
|
className: `cm-s-solarized cm-s-dark`
|
||||||
|
}, {
|
||||||
|
name: 'solarized light',
|
||||||
|
path: path.join(paths[0], 'solarized.css'),
|
||||||
|
className: `cm-s-solarized cm-s-light`
|
||||||
|
})
|
||||||
|
themes.splice(0, 0, {
|
||||||
|
name: 'default',
|
||||||
|
path: path.join(paths[0], 'elegant.css'),
|
||||||
|
className: `cm-s-default`
|
||||||
|
})
|
||||||
|
|
||||||
const snippetFile = process.env.NODE_ENV !== 'test'
|
const snippetFile = process.env.NODE_ENV !== 'test'
|
||||||
? path.join(app.getPath('userData'), 'snippets.json')
|
? path.join(app.getPath('userData'), 'snippets.json')
|
||||||
@@ -35,7 +64,7 @@ const consts = {
|
|||||||
'Dodger Blue',
|
'Dodger Blue',
|
||||||
'Violet Eggplant'
|
'Violet Eggplant'
|
||||||
],
|
],
|
||||||
THEMES: ['default'].concat(themes),
|
THEMES: themes,
|
||||||
SNIPPET_FILE: snippetFile,
|
SNIPPET_FILE: snippetFile,
|
||||||
DEFAULT_EDITOR_FONT_FAMILY: [
|
DEFAULT_EDITOR_FONT_FAMILY: [
|
||||||
'Monaco',
|
'Monaco',
|
||||||
|
|||||||
124
browser/lib/contextMenuBuilder.js
Normal file
124
browser/lib/contextMenuBuilder.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
const {remote} = require('electron')
|
||||||
|
const {Menu} = remote.require('electron')
|
||||||
|
const {clipboard} = remote.require('electron')
|
||||||
|
const {shell} = remote.require('electron')
|
||||||
|
const spellcheck = require('./spellcheck')
|
||||||
|
const uri2path = require('file-uri-to-path')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
|
||||||
|
* If the word is does not contains a spelling error (determined by the 'error style'), no suggestions for corrections are requested
|
||||||
|
* => they are not visible in the context menu
|
||||||
|
* @param editor CodeMirror editor
|
||||||
|
* @param {MouseEvent} event that has triggered the creation of the context menu
|
||||||
|
* @returns {Electron.Menu} The created electron context menu
|
||||||
|
*/
|
||||||
|
const buildEditorContextMenu = function (editor, event) {
|
||||||
|
if (editor == null || event == null || event.pageX == null || event.pageY == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY})
|
||||||
|
const wordRange = editor.findWordAt(cursor)
|
||||||
|
const word = editor.getRange(wordRange.anchor, wordRange.head)
|
||||||
|
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
|
||||||
|
let isMisspelled = false
|
||||||
|
for (const mark of existingMarks) {
|
||||||
|
if (mark.className === spellcheck.getCSSClassName()) {
|
||||||
|
isMisspelled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let suggestion = []
|
||||||
|
if (isMisspelled) {
|
||||||
|
suggestion = spellcheck.getSpellingSuggestion(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = {
|
||||||
|
isMisspelled: isMisspelled,
|
||||||
|
spellingSuggestions: suggestion
|
||||||
|
}
|
||||||
|
const template = [{
|
||||||
|
role: 'cut'
|
||||||
|
}, {
|
||||||
|
role: 'copy'
|
||||||
|
}, {
|
||||||
|
role: 'paste'
|
||||||
|
}, {
|
||||||
|
role: 'selectall'
|
||||||
|
}]
|
||||||
|
|
||||||
|
if (selection.isMisspelled) {
|
||||||
|
const suggestions = selection.spellingSuggestions
|
||||||
|
template.unshift.apply(template, suggestions.map(function (suggestion) {
|
||||||
|
return {
|
||||||
|
label: suggestion,
|
||||||
|
click: function (suggestion) {
|
||||||
|
if (editor != null) {
|
||||||
|
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).concat({
|
||||||
|
type: 'separator'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return Menu.buildFromTemplate(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the context menu that is shown when there is a right click Markdown preview of a (not-snippet) note.
|
||||||
|
* @param {MarkdownPreview} markdownPreview
|
||||||
|
* @param {MouseEvent} event that has triggered the creation of the context menu
|
||||||
|
* @returns {Electron.Menu} The created electron context menu
|
||||||
|
*/
|
||||||
|
const buildMarkdownPreviewContextMenu = function (markdownPreview, event) {
|
||||||
|
if (markdownPreview == null || event == null || event.pageX == null || event.pageY == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default context menu inclusions
|
||||||
|
const template = [{
|
||||||
|
role: 'copy'
|
||||||
|
}, {
|
||||||
|
role: 'selectall'
|
||||||
|
}]
|
||||||
|
|
||||||
|
if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) {
|
||||||
|
// Link opener for files on the local system pointed to by href
|
||||||
|
const href = event.target.href
|
||||||
|
const isLocalFile = href.startsWith('file:')
|
||||||
|
if (isLocalFile) {
|
||||||
|
const absPath = uri2path(href)
|
||||||
|
try {
|
||||||
|
if (fs.lstatSync(absPath).isFile()) {
|
||||||
|
template.push(
|
||||||
|
{
|
||||||
|
label: i18n.__('Show in explorer'),
|
||||||
|
click: (e) => shell.showItemInFolder(absPath)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error while evaluating if the file is locally available', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add option to context menu to copy url
|
||||||
|
template.push(
|
||||||
|
{
|
||||||
|
label: i18n.__('Copy Url'),
|
||||||
|
click: (e) => clipboard.writeText(href)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Menu.buildFromTemplate(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
{
|
||||||
|
buildEditorContextMenu: buildEditorContextMenu,
|
||||||
|
buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
import 'codemirror-mode-elixir'
|
import 'codemirror-mode-elixir'
|
||||||
|
|
||||||
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
const stylusCodeInfo = CodeMirror.modeInfo.find(info => info.name === 'Stylus')
|
||||||
|
if (stylusCodeInfo == null) {
|
||||||
|
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
||||||
|
} else {
|
||||||
|
stylusCodeInfo.alias = ['styl']
|
||||||
|
}
|
||||||
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})
|
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ export function getTodoStatus (content) {
|
|||||||
let numberOfCompletedTodo = 0
|
let numberOfCompletedTodo = 0
|
||||||
|
|
||||||
splitted.forEach((line) => {
|
splitted.forEach((line) => {
|
||||||
const trimmedLine = line.trim()
|
const trimmedLine = line.trim().replace(/^(>\s*)*/, '')
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
|
if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) {
|
||||||
numberOfTodo++
|
numberOfTodo++
|
||||||
}
|
}
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
|
if (trimmedLine.match(/^[+\-*] \[x] ./i)) {
|
||||||
numberOfCompletedTodo++
|
numberOfCompletedTodo++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const _ = require('lodash')
|
|
||||||
const uuidv4 = require('uuid/v4')
|
const uuidv4 = require('uuid/v4')
|
||||||
|
|
||||||
module.exports = function (uuid) {
|
module.exports = function (uuid) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module.exports = function (md, renderers, defaultRenderer) {
|
module.exports = function (md, renderers, defaultRenderer) {
|
||||||
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
|
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
|
||||||
|
|
||||||
function fence (state, startLine, endLine) {
|
function fence (state, startLine, endLine, silent) {
|
||||||
let pos = state.bMarks[startLine] + state.tShift[startLine]
|
let pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||||
let max = state.eMarks[startLine]
|
let max = state.eMarks[startLine]
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ module.exports = function (md, renderers, defaultRenderer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const marker = state.src.charCodeAt(pos)
|
const marker = state.src.charCodeAt(pos)
|
||||||
if (!(marker === 96 || marker === 126)) {
|
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +27,10 @@ module.exports = function (md, renderers, defaultRenderer) {
|
|||||||
const markup = state.src.slice(mem, pos)
|
const markup = state.src.slice(mem, pos)
|
||||||
const params = state.src.slice(pos, max)
|
const params = state.src.slice(pos, max)
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
let nextLine = startLine
|
let nextLine = startLine
|
||||||
let haveEndMarker = false
|
let haveEndMarker = false
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) {
|
|||||||
options
|
options
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.tokens[tokenIdx].type === '_fence') {
|
if (state.tokens[tokenIdx].type.match(/.*_fence$/)) {
|
||||||
// escapeHtmlCharacters has better performance
|
// escapeHtmlCharacters has better performance
|
||||||
state.tokens[tokenIdx].content = escapeHtmlCharacters(
|
state.tokens[tokenIdx].content = escapeHtmlCharacters(
|
||||||
state.tokens[tokenIdx].content,
|
state.tokens[tokenIdx].content,
|
||||||
@@ -96,6 +96,10 @@ function sanitizeInline (html, options) {
|
|||||||
|
|
||||||
function naughtyHRef (href, options) {
|
function naughtyHRef (href, options) {
|
||||||
// href = href.replace(/[\x00-\x20]+/g, '')
|
// href = href.replace(/[\x00-\x20]+/g, '')
|
||||||
|
if (!href) {
|
||||||
|
// No href
|
||||||
|
return false
|
||||||
|
}
|
||||||
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
|
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
|
||||||
|
|
||||||
const matches = href.match(/^([a-zA-Z]+)\:/)
|
const matches = href.match(/^([a-zA-Z]+)\:/)
|
||||||
|
|||||||
@@ -2,49 +2,34 @@
|
|||||||
* @fileoverview Markdown table of contents generator
|
* @fileoverview Markdown table of contents generator
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { EOL } from 'os'
|
||||||
import toc from 'markdown-toc'
|
import toc from 'markdown-toc'
|
||||||
import diacritics from 'diacritics-map'
|
import mdlink from 'markdown-link'
|
||||||
import stripColor from 'strip-color'
|
import slugify from './slugify'
|
||||||
|
|
||||||
const EOL = require('os').EOL
|
const hasProp = Object.prototype.hasOwnProperty
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @caseSensitiveSlugify Custom slugify function
|
* From @enyaxu/markdown-it-anchor
|
||||||
* Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js),
|
|
||||||
* but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067
|
|
||||||
*/
|
*/
|
||||||
function caseSensitiveSlugify (str) {
|
function uniqueSlug (slug, slugs, opts) {
|
||||||
function replaceDiacritics (str) {
|
let uniq = slug
|
||||||
return str.replace(/[À-ž]/g, function (ch) {
|
let i = opts.uniqueSlugStartIndex
|
||||||
return diacritics[ch] || ch
|
while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
|
||||||
})
|
slugs[uniq] = true
|
||||||
}
|
return uniq
|
||||||
|
}
|
||||||
|
|
||||||
function getTitle (str) {
|
function linkify (token) {
|
||||||
if (/^\[[^\]]+\]\(/.test(str)) {
|
token.content = mdlink(token.content, `#${decodeURI(token.slug)}`)
|
||||||
var m = /^\[([^\]]+)\]/.exec(str)
|
return token
|
||||||
if (m) return m[1]
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
str = getTitle(str)
|
|
||||||
str = stripColor(str)
|
|
||||||
// str = str.toLowerCase() //let's be case sensitive
|
|
||||||
|
|
||||||
// `.split()` is often (but not always) faster than `.replace()`
|
|
||||||
str = str.split(' ').join('-')
|
|
||||||
str = str.split(/\t/).join('--')
|
|
||||||
str = str.split(/<\/?[^>]+>/).join('')
|
|
||||||
str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('')
|
|
||||||
str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('')
|
|
||||||
str = replaceDiacritics(str)
|
|
||||||
return str
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOC_MARKER_START = '<!-- toc -->'
|
const TOC_MARKER_START = '<!-- toc -->'
|
||||||
const TOC_MARKER_END = '<!-- tocstop -->'
|
const TOC_MARKER_END = '<!-- tocstop -->'
|
||||||
|
|
||||||
|
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes care of proper updating given editor with TOC.
|
* Takes care of proper updating given editor with TOC.
|
||||||
* If TOC doesn't exit in the editor, it's inserted at current caret position.
|
* If TOC doesn't exit in the editor, it's inserted at current caret position.
|
||||||
@@ -52,12 +37,6 @@ const TOC_MARKER_END = '<!-- tocstop -->'
|
|||||||
* @param editor CodeMirror editor to be updated with TOC
|
* @param editor CodeMirror editor to be updated with TOC
|
||||||
*/
|
*/
|
||||||
export function generateInEditor (editor) {
|
export function generateInEditor (editor) {
|
||||||
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
|
|
||||||
|
|
||||||
function tocExistsInEditor () {
|
|
||||||
return tocRegex.test(editor.getValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateExistingToc () {
|
function updateExistingToc () {
|
||||||
const toc = generate(editor.getValue())
|
const toc = generate(editor.getValue())
|
||||||
const search = editor.getSearchCursor(tocRegex)
|
const search = editor.getSearchCursor(tocRegex)
|
||||||
@@ -71,21 +50,40 @@ export function generateInEditor (editor) {
|
|||||||
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tocExistsInEditor()) {
|
if (tocExistsInEditor(editor)) {
|
||||||
updateExistingToc()
|
updateExistingToc()
|
||||||
} else {
|
} else {
|
||||||
addTocAtCursorPosition()
|
addTocAtCursorPosition()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function tocExistsInEditor (editor) {
|
||||||
|
return tocRegex.test(editor.getValue())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates MD TOC based on MD document passed as string.
|
* Generates MD TOC based on MD document passed as string.
|
||||||
* @param markdownText MD document
|
* @param markdownText MD document
|
||||||
* @returns generatedTOC String containing generated TOC
|
* @returns generatedTOC String containing generated TOC
|
||||||
*/
|
*/
|
||||||
export function generate (markdownText) {
|
export function generate (markdownText) {
|
||||||
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify})
|
const slugs = {}
|
||||||
return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END
|
const opts = {
|
||||||
|
uniqueSlugStartIndex: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = toc(markdownText, {
|
||||||
|
slugify: title => {
|
||||||
|
return uniqueSlug(slugify(title), slugs, opts)
|
||||||
|
},
|
||||||
|
linkify: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const md = toc.bullets(result.json.map(linkify), {
|
||||||
|
highest: result.highest
|
||||||
|
})
|
||||||
|
|
||||||
|
return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapTocWithEol (toc, editor) {
|
function wrapTocWithEol (toc, editor) {
|
||||||
@@ -96,5 +94,6 @@ function wrapTocWithEol (toc, editor) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
generate,
|
generate,
|
||||||
generateInEditor
|
generateInEditor,
|
||||||
|
tocExistsInEditor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import markdownit from 'markdown-it'
|
|||||||
import sanitize from './markdown-it-sanitize-html'
|
import sanitize from './markdown-it-sanitize-html'
|
||||||
import emoji from 'markdown-it-emoji'
|
import emoji from 'markdown-it-emoji'
|
||||||
import math from '@rokt33r/markdown-it-math'
|
import math from '@rokt33r/markdown-it-math'
|
||||||
|
import mdurl from 'mdurl'
|
||||||
import smartArrows from 'markdown-it-smartarrows'
|
import smartArrows from 'markdown-it-smartarrows'
|
||||||
|
import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import katex from 'katex'
|
import katex from 'katex'
|
||||||
import { lastFindInArray } from './utils'
|
import { lastFindInArray } from './utils'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
|
||||||
|
|
||||||
function createGutter (str, firstLineNumber) {
|
function createGutter (str, firstLineNumber) {
|
||||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||||
@@ -33,6 +34,7 @@ class Markdown {
|
|||||||
|
|
||||||
const updatedOptions = Object.assign(defaultOptions, options)
|
const updatedOptions = Object.assign(defaultOptions, options)
|
||||||
this.md = markdownit(updatedOptions)
|
this.md = markdownit(updatedOptions)
|
||||||
|
this.md.linkify.set({ fuzzyLink: false })
|
||||||
|
|
||||||
if (updatedOptions.sanitize !== 'NONE') {
|
if (updatedOptions.sanitize !== 'NONE') {
|
||||||
const allowedTags = ['iframe', 'input', 'b',
|
const allowedTags = ['iframe', 'input', 'b',
|
||||||
@@ -118,19 +120,20 @@ class Markdown {
|
|||||||
this.md.use(require('markdown-it-imsize'))
|
this.md.use(require('markdown-it-imsize'))
|
||||||
this.md.use(require('markdown-it-footnote'))
|
this.md.use(require('markdown-it-footnote'))
|
||||||
this.md.use(require('markdown-it-multimd-table'))
|
this.md.use(require('markdown-it-multimd-table'))
|
||||||
this.md.use(require('markdown-it-named-headers'), {
|
this.md.use(require('@enyaxu/markdown-it-anchor'), {
|
||||||
slugify: (header) => {
|
slugify: require('./slugify')
|
||||||
return encodeURI(header.trim()
|
|
||||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
|
||||||
.replace(/\s+/g, '-'))
|
|
||||||
.replace(/\-+$/, '')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
this.md.use(require('markdown-it-kbd'))
|
this.md.use(require('markdown-it-kbd'))
|
||||||
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
|
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error', 'quote', 'abstract', 'question']})
|
||||||
this.md.use(require('markdown-it-abbr'))
|
this.md.use(require('markdown-it-abbr'))
|
||||||
this.md.use(require('markdown-it-sub'))
|
this.md.use(require('markdown-it-sub'))
|
||||||
this.md.use(require('markdown-it-sup'))
|
this.md.use(require('markdown-it-sup'))
|
||||||
|
this.md.use(markdownItTocAndAnchor, {
|
||||||
|
toc: true,
|
||||||
|
tocPattern: /\[TOC\]/i,
|
||||||
|
anchorLink: false,
|
||||||
|
appendIdToHeading: false
|
||||||
|
})
|
||||||
this.md.use(require('./markdown-it-deflist'))
|
this.md.use(require('./markdown-it-deflist'))
|
||||||
this.md.use(require('./markdown-it-frontmatter'))
|
this.md.use(require('./markdown-it-frontmatter'))
|
||||||
|
|
||||||
@@ -151,6 +154,21 @@ class Markdown {
|
|||||||
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
|
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
},
|
},
|
||||||
|
gallery: token => {
|
||||||
|
const content = token.content.split('\n').slice(0, -1).map(line => {
|
||||||
|
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
|
||||||
|
if (match) {
|
||||||
|
return mdurl.encode(match[1])
|
||||||
|
} else {
|
||||||
|
return mdurl.encode(line)
|
||||||
|
}
|
||||||
|
}).join('\n')
|
||||||
|
|
||||||
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
|
<span class="filename">${token.fileName}</span>
|
||||||
|
<div class="gallery" data-autoplay="${token.parameters.autoplay}" data-height="${token.parameters.height}">${content}</div>
|
||||||
|
</pre>`
|
||||||
|
},
|
||||||
mermaid: token => {
|
mermaid: token => {
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
@@ -172,32 +190,47 @@ class Markdown {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||||
this.md.use(require('markdown-it-plantuml'), '', {
|
const plantuml = require('markdown-it-plantuml')
|
||||||
generateSource: function (umlCode) {
|
const plantUmlStripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
||||||
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
const plantUmlServerAddress = plantUmlStripTrailingSlash(config.preview.plantUMLServerAddress)
|
||||||
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg'
|
const parsePlantUml = function (umlCode, openMarker, closeMarker, type) {
|
||||||
const s = unescape(encodeURIComponent(umlCode))
|
const s = unescape(encodeURIComponent(umlCode))
|
||||||
const zippedCode = deflate.encode64(
|
const zippedCode = deflate.encode64(
|
||||||
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9)
|
||||||
)
|
)
|
||||||
return `${serverAddress}/${zippedCode}`
|
return `${plantUmlServerAddress}/${type}/${zippedCode}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.md.use(plantuml, {
|
||||||
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startuml', '@enduml', 'svg')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ditaa support
|
// Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
||||||
this.md.use(require('markdown-it-plantuml'), {
|
this.md.use(plantuml, {
|
||||||
openMarker: '@startditaa',
|
openMarker: '@startditaa',
|
||||||
closeMarker: '@endditaa',
|
closeMarker: '@endditaa',
|
||||||
generateSource: function (umlCode) {
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
|
||||||
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
})
|
||||||
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
|
||||||
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
|
// Mindmap support
|
||||||
const s = unescape(encodeURIComponent(umlCode))
|
this.md.use(plantuml, {
|
||||||
const zippedCode = deflate.encode64(
|
openMarker: '@startmindmap',
|
||||||
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9)
|
closeMarker: '@endmindmap',
|
||||||
)
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg')
|
||||||
return `${serverAddress}/${zippedCode}`
|
})
|
||||||
}
|
|
||||||
|
// WBS support
|
||||||
|
this.md.use(plantuml, {
|
||||||
|
openMarker: '@startwbs',
|
||||||
|
closeMarker: '@endwbs',
|
||||||
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Gantt support
|
||||||
|
this.md.use(plantuml, {
|
||||||
|
openMarker: '@startgantt',
|
||||||
|
closeMarker: '@endgantt',
|
||||||
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Override task item
|
// Override task item
|
||||||
@@ -278,7 +311,9 @@ class Markdown {
|
|||||||
case 'list_item_open':
|
case 'list_item_open':
|
||||||
case 'paragraph_open':
|
case 'paragraph_open':
|
||||||
case 'table_open':
|
case 'table_open':
|
||||||
token.attrPush(['data-line', token.map[0]])
|
if (token.map) {
|
||||||
|
token.attrPush(['data-line', token.map[0]])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function strip (input) {
|
|||||||
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
|
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
|
||||||
.replace(/>/g, '')
|
.replace(/>/g, '')
|
||||||
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
|
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
|
||||||
.replace(/^#{1,6}\s*([^#]*)\s*(#{1,6})?/gm, '$1')
|
.replace(/^#{1,6}\s*/gm, '')
|
||||||
.replace(/(`{3,})(.*?)\1/gm, '$2')
|
.replace(/(`{3,})(.*?)\1/gm, '$2')
|
||||||
.replace(/^-{3,}\s*$/g, '')
|
.replace(/^-{3,}\s*$/g, '')
|
||||||
.replace(/`(.+?)`/g, '$1')
|
.replace(/`(.+?)`/g, '$1')
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
import { hashHistory } from 'react-router'
|
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
export function createMarkdownNote (storage, folder, dispatch, location) {
|
export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||||
|
|
||||||
|
let tags = []
|
||||||
|
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
|
||||||
|
tags = params.tagname.split(' ')
|
||||||
|
}
|
||||||
|
|
||||||
return dataApi
|
return dataApi
|
||||||
.createNote(storage, {
|
.createNote(storage, {
|
||||||
type: 'MARKDOWN_NOTE',
|
type: 'MARKDOWN_NOTE',
|
||||||
folder: folder,
|
folder: folder,
|
||||||
title: '',
|
title: '',
|
||||||
content: ''
|
tags,
|
||||||
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
})
|
})
|
||||||
.then(note => {
|
.then(note => {
|
||||||
const noteHash = note.key
|
const noteHash = note.key
|
||||||
@@ -20,29 +29,39 @@ export function createMarkdownNote (storage, folder, dispatch, location) {
|
|||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
|
|
||||||
hashHistory.push({
|
dispatch(push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: { key: noteHash }
|
search: queryString.stringify({ key: noteHash })
|
||||||
})
|
}))
|
||||||
ee.emit('list:jump', noteHash)
|
ee.emit('list:jump', noteHash)
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSnippetNote (storage, folder, dispatch, location, config) {
|
export function createSnippetNote (storage, folder, dispatch, location, params, config) {
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||||
|
|
||||||
|
let tags = []
|
||||||
|
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
|
||||||
|
tags = params.tagname.split(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLanguage = config.editor.snippetDefaultLanguage === 'Auto Detect' ? null : config.editor.snippetDefaultLanguage
|
||||||
|
|
||||||
return dataApi
|
return dataApi
|
||||||
.createNote(storage, {
|
.createNote(storage, {
|
||||||
type: 'SNIPPET_NOTE',
|
type: 'SNIPPET_NOTE',
|
||||||
folder: folder,
|
folder: folder,
|
||||||
title: '',
|
title: '',
|
||||||
|
tags,
|
||||||
description: '',
|
description: '',
|
||||||
snippets: [
|
snippets: [
|
||||||
{
|
{
|
||||||
name: '',
|
name: '',
|
||||||
mode: config.editor.snippetDefaultLanguage || 'text',
|
mode: defaultLanguage,
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -52,10 +71,10 @@ export function createSnippetNote (storage, folder, dispatch, location, config)
|
|||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
hashHistory.push({
|
dispatch(push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: { key: noteHash }
|
search: queryString.stringify({ key: noteHash })
|
||||||
})
|
}))
|
||||||
ee.emit('list:jump', noteHash)
|
ee.emit('list:jump', noteHash)
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
})
|
})
|
||||||
|
|||||||
11
browser/lib/slugify.js
Normal file
11
browser/lib/slugify.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = function slugify (title) {
|
||||||
|
const slug = encodeURI(
|
||||||
|
title.trim()
|
||||||
|
.replace(/^\s+/, '')
|
||||||
|
.replace(/\s+$/, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g, '')
|
||||||
|
)
|
||||||
|
|
||||||
|
return slug
|
||||||
|
}
|
||||||
232
browser/lib/spellcheck.js
Normal file
232
browser/lib/spellcheck.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import styles from '../components/CodeEditor.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const Typo = require('typo-js')
|
||||||
|
const _ = require('lodash')
|
||||||
|
|
||||||
|
const CSS_ERROR_CLASS = 'codeEditor-typo'
|
||||||
|
const SPELLCHECK_DISABLED = 'NONE'
|
||||||
|
const DICTIONARY_PATH = '../dictionaries'
|
||||||
|
const MILLISECONDS_TILL_LIVECHECK = 500
|
||||||
|
|
||||||
|
let dictionary = null
|
||||||
|
let self
|
||||||
|
|
||||||
|
function getAvailableDictionaries () {
|
||||||
|
return [
|
||||||
|
{label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED},
|
||||||
|
{label: i18n.__('English'), value: 'en_GB'},
|
||||||
|
{label: i18n.__('German'), value: 'de_DE'},
|
||||||
|
{label: i18n.__('French'), value: 'fr_FR'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only to be used in the tests :)
|
||||||
|
*/
|
||||||
|
function setDictionaryForTestsOnly (newDictionary) {
|
||||||
|
dictionary = newDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Initializes the spellcheck. It removes all existing marks of the current editor.
|
||||||
|
* If a language was given (i.e. lang !== this.SPELLCHECK_DISABLED) it will load the stated dictionary and use it to check the whole document.
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param {String} lang on of the values from getAvailableDictionaries()-Method
|
||||||
|
*/
|
||||||
|
function setLanguage (editor, lang) {
|
||||||
|
self = this
|
||||||
|
dictionary = null
|
||||||
|
|
||||||
|
if (editor == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMarks = editor.getAllMarks() || []
|
||||||
|
for (const mark of existingMarks) {
|
||||||
|
mark.clear()
|
||||||
|
}
|
||||||
|
if (lang !== SPELLCHECK_DISABLED) {
|
||||||
|
dictionary = new Typo(lang, false, false, {
|
||||||
|
dictionaryPath: DICTIONARY_PATH,
|
||||||
|
asyncLoad: true,
|
||||||
|
loadedCallback: () =>
|
||||||
|
checkWholeDocument(editor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the whole content of the editor for typos
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
*/
|
||||||
|
function checkWholeDocument (editor) {
|
||||||
|
const lastLine = editor.lineCount() - 1
|
||||||
|
const textOfLastLine = editor.getLine(lastLine) || ''
|
||||||
|
const lastChar = textOfLastLine.length
|
||||||
|
const from = {line: 0, ch: 0}
|
||||||
|
const to = {line: lastLine, ch: lastChar}
|
||||||
|
checkMultiLineRange(editor, from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the given range for typos
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param {line, ch} from starting position of the spellcheck
|
||||||
|
* @param {line, ch} to end position of the spellcheck
|
||||||
|
*/
|
||||||
|
function checkMultiLineRange (editor, from, to) {
|
||||||
|
function sortRange (pos1, pos2) {
|
||||||
|
if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) {
|
||||||
|
return {from: pos2, to: pos1}
|
||||||
|
}
|
||||||
|
return {from: pos1, to: pos2}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {from: smallerPos, to: higherPos} = sortRange(from, to)
|
||||||
|
for (let l = smallerPos.line; l <= higherPos.line; l++) {
|
||||||
|
const line = editor.getLine(l) || ''
|
||||||
|
let w = 0
|
||||||
|
if (l === smallerPos.line) {
|
||||||
|
w = smallerPos.ch
|
||||||
|
}
|
||||||
|
let wEnd = line.length
|
||||||
|
if (l === higherPos.line) {
|
||||||
|
wEnd = higherPos.ch
|
||||||
|
}
|
||||||
|
while (w <= wEnd) {
|
||||||
|
const wordRange = editor.findWordAt({line: l, ch: w})
|
||||||
|
self.checkWord(editor, wordRange)
|
||||||
|
w += (wordRange.head.ch - wordRange.anchor.ch) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Checks whether a certain range of characters in the editor (i.e. a word) contains a typo.
|
||||||
|
* If so the ranged will be marked with the class CSS_ERROR_CLASS.
|
||||||
|
* Note: Due to performance considerations, only words with more then 3 signs are checked.
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param wordRange Object specifying the range that should be checked.
|
||||||
|
* Having the following structure: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
|
||||||
|
*/
|
||||||
|
function checkWord (editor, wordRange) {
|
||||||
|
const word = editor.getRange(wordRange.anchor, wordRange.head)
|
||||||
|
if (word == null || word.length <= 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!dictionary.check(word)) {
|
||||||
|
editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the changes recently made (aka live check)
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param fromChangeObject codeMirror changeObject describing the start of the editing
|
||||||
|
* @param toChangeObject codeMirror changeObject describing the end of the editing
|
||||||
|
*/
|
||||||
|
function checkChangeRange (editor, fromChangeObject, toChangeObject) {
|
||||||
|
/**
|
||||||
|
* Calculate the smallest respectively largest position as a start, resp. end, position and return it
|
||||||
|
* @param start CodeMirror change object
|
||||||
|
* @param end CodeMirror change object
|
||||||
|
* @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
|
||||||
|
*/
|
||||||
|
function getStartAndEnd (start, end) {
|
||||||
|
const possiblePositions = [start.from, start.to, end.from, end.to]
|
||||||
|
let smallest = start.from
|
||||||
|
let biggest = end.to
|
||||||
|
for (const currentPos of possiblePositions) {
|
||||||
|
if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) {
|
||||||
|
smallest = currentPos
|
||||||
|
}
|
||||||
|
if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) {
|
||||||
|
biggest = currentPos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {start: smallest, end: biggest}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dictionary === null || editor == null) { return }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject)
|
||||||
|
|
||||||
|
// Expand the range to include words after/before whitespaces
|
||||||
|
start.ch = Math.max(start.ch - 1, 0)
|
||||||
|
end.ch = end.ch + 1
|
||||||
|
|
||||||
|
// clean existing marks
|
||||||
|
const existingMarks = editor.findMarks(start, end) || []
|
||||||
|
for (const mark of existingMarks) {
|
||||||
|
mark.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.checkMultiLineRange(editor, start, end)
|
||||||
|
} catch (e) {
|
||||||
|
console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLiveSpellCheckFrom (changeObject) {
|
||||||
|
liveSpellCheckFrom = changeObject
|
||||||
|
}
|
||||||
|
let liveSpellCheckFrom
|
||||||
|
const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, {
|
||||||
|
'leading': true,
|
||||||
|
'trailing': false
|
||||||
|
})
|
||||||
|
const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, {
|
||||||
|
'leading': false,
|
||||||
|
'trailing': true
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param changeObject codeMirror changeObject
|
||||||
|
*/
|
||||||
|
function handleChange (editor, changeObject) {
|
||||||
|
if (dictionary === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
debouncedSpellCheckLeading(changeObject)
|
||||||
|
debouncedSpellCheck(editor, liveSpellCheckFrom, changeObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of spelling suggestions for the given (wrong written) word.
|
||||||
|
* Returns an empty array if the dictionary is null (=> spellcheck is disabled) or the given word was null
|
||||||
|
* @param word word to be checked
|
||||||
|
* @returns {String[]} Array of suggestions
|
||||||
|
*/
|
||||||
|
function getSpellingSuggestion (word) {
|
||||||
|
if (dictionary == null || word == null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return dictionary.suggest(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the CSS class used for errors
|
||||||
|
*/
|
||||||
|
function getCSSClassName () {
|
||||||
|
return styles[CSS_ERROR_CLASS]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DICTIONARY_PATH,
|
||||||
|
CSS_ERROR_CLASS,
|
||||||
|
SPELLCHECK_DISABLED,
|
||||||
|
getAvailableDictionaries,
|
||||||
|
setLanguage,
|
||||||
|
checkChangeRange,
|
||||||
|
handleChange,
|
||||||
|
getSpellingSuggestion,
|
||||||
|
checkWord,
|
||||||
|
checkMultiLineRange,
|
||||||
|
checkWholeDocument,
|
||||||
|
setDictionaryForTestsOnly,
|
||||||
|
getCSSClassName
|
||||||
|
}
|
||||||
9
browser/lib/turndown.js
Normal file
9
browser/lib/turndown.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const TurndownService = require('turndown')
|
||||||
|
const { gfm } = require('turndown-plugin-gfm')
|
||||||
|
|
||||||
|
export const createTurndownService = function () {
|
||||||
|
const turndown = new TurndownService()
|
||||||
|
turndown.use(gfm)
|
||||||
|
turndown.remove('script')
|
||||||
|
return turndown
|
||||||
|
}
|
||||||
@@ -132,8 +132,28 @@ export function isObjectEqual (a, b) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMarkdownTitleURL (str) {
|
||||||
|
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanFileSize (bytes) {
|
||||||
|
const threshold = 1000
|
||||||
|
if (Math.abs(bytes) < threshold) {
|
||||||
|
return bytes + ' B'
|
||||||
|
}
|
||||||
|
var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
var u = -1
|
||||||
|
do {
|
||||||
|
bytes /= threshold
|
||||||
|
++u
|
||||||
|
} while (Math.abs(bytes) >= threshold && u < units.length - 1)
|
||||||
|
return bytes.toFixed(1) + ' ' + units[u]
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
lastFindInArray,
|
lastFindInArray,
|
||||||
escapeHtmlCharacters,
|
escapeHtmlCharacters,
|
||||||
isObjectEqual
|
isObjectEqual,
|
||||||
|
isMarkdownTitleURL,
|
||||||
|
humanFileSize
|
||||||
}
|
}
|
||||||
|
|||||||
69
browser/main/Detail/FromUrlButton.js
Normal file
69
browser/main/Detail/FromUrlButton.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './FromUrlButton.styl'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
class FromUrlButton extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isActive: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown (e) {
|
||||||
|
this.setState({
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp (e) {
|
||||||
|
this.setState({
|
||||||
|
isActive: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave (e) {
|
||||||
|
this.setState({
|
||||||
|
isActive: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { className } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={_.isString(className)
|
||||||
|
? 'FromUrlButton ' + className
|
||||||
|
: 'FromUrlButton'
|
||||||
|
}
|
||||||
|
styleName={this.state.isActive || this.props.isActive
|
||||||
|
? 'root--active'
|
||||||
|
: 'root'
|
||||||
|
}
|
||||||
|
onMouseDown={(e) => this.handleMouseDown(e)}
|
||||||
|
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||||
|
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
||||||
|
onClick={this.props.onClick}>
|
||||||
|
<img styleName='icon'
|
||||||
|
src={this.state.isActive || this.props.isActive
|
||||||
|
? '../resources/icon/icon-external.svg'
|
||||||
|
: '../resources/icon/icon-external.svg'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span styleName='tooltip'>{i18n.__('Convert URL to Markdown')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FromUrlButton.propTypes = {
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
className: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(FromUrlButton, styles)
|
||||||
41
browser/main/Detail/FromUrlButton.styl
Normal file
41
browser/main/Detail/FromUrlButton.styl
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.root
|
||||||
|
top 45px
|
||||||
|
topBarButtonRight()
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 125px
|
||||||
|
width 90px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
@extend .root
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-favorite-star-button-color
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
|
|
||||||
|
.icon
|
||||||
|
transition transform 0.15s
|
||||||
|
height 13px
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
topBarButtonDark()
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
@@ -5,15 +5,17 @@ import styles from './FullscreenButton.styl'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
|
||||||
const FullscreenButton = ({
|
const FullscreenButton = ({
|
||||||
onClick
|
onClick
|
||||||
}) => (
|
}) => {
|
||||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
return (
|
||||||
<span styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||||
</button>
|
<img src='../resources/icon/icon-full.svg' />
|
||||||
)
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
FullscreenButton.propTypes = {
|
FullscreenButton.propTypes = {
|
||||||
onClick: PropTypes.func.isRequired
|
onClick: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
|
.tooltip:lang(ja)
|
||||||
|
@extend .tooltip
|
||||||
|
right 35px
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
@@ -14,7 +14,7 @@ class InfoPanel extends React.Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
|
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf, wordCount, letterCount, type, print
|
||||||
} = this.props
|
} = this.props
|
||||||
return (
|
return (
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||||
@@ -60,7 +60,7 @@ class InfoPanel extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
|
<input styleName='infoPanel-noteLink' ref='noteLink' defaultValue={noteLink} onClick={(e) => { e.target.select() }} />
|
||||||
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
||||||
<i className='fa fa-clipboard' />
|
<i className='fa fa-clipboard' />
|
||||||
</button>
|
</button>
|
||||||
@@ -70,22 +70,27 @@ class InfoPanel extends React.Component {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div id='export-wrap'>
|
<div id='export-wrap'>
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
||||||
<i className='fa fa-file-code-o' />
|
<i className='fa fa-file-code-o' />
|
||||||
<p>{i18n.__('.md')}</p>
|
<p>{i18n.__('.md')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
||||||
<i className='fa fa-file-text-o' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>{i18n.__('.txt')}</p>
|
<p>{i18n.__('.txt')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||||
<i className='fa fa-html5' />
|
<i className='fa fa-html5' />
|
||||||
<p>{i18n.__('.html')}</p>
|
<p>{i18n.__('.html')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => print(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
|
||||||
|
<i className='fa fa-file-pdf-o' />
|
||||||
|
<p>{i18n.__('.pdf')}</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
|
||||||
<i className='fa fa-print' />
|
<i className='fa fa-print' />
|
||||||
<p>{i18n.__('Print')}</p>
|
<p>{i18n.__('Print')}</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -104,6 +109,7 @@ InfoPanel.propTypes = {
|
|||||||
exportAsMd: PropTypes.func.isRequired,
|
exportAsMd: PropTypes.func.isRequired,
|
||||||
exportAsTxt: PropTypes.func.isRequired,
|
exportAsTxt: PropTypes.func.isRequired,
|
||||||
exportAsHtml: PropTypes.func.isRequired,
|
exportAsHtml: PropTypes.func.isRequired,
|
||||||
|
exportAsPdf: PropTypes.func.isRequired,
|
||||||
wordCount: PropTypes.number,
|
wordCount: PropTypes.number,
|
||||||
letterCount: PropTypes.number,
|
letterCount: PropTypes.number,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
right 25px
|
right 25px
|
||||||
position absolute
|
position absolute
|
||||||
padding 20px 25px 0 25px
|
padding 20px 25px 0 25px
|
||||||
width 300px
|
// width 300px
|
||||||
overflow auto
|
overflow auto
|
||||||
background-color $ui-noteList-backgroundColor
|
background-color $ui-noteList-backgroundColor
|
||||||
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import styles from './InfoPanel.styl'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoPanelTrashed = ({
|
const InfoPanelTrashed = ({
|
||||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
|
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf
|
||||||
}) => (
|
}) => (
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
||||||
<div>
|
<div>
|
||||||
@@ -31,22 +31,22 @@ const InfoPanelTrashed = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='export-wrap'>
|
<div id='export-wrap'>
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
||||||
<i className='fa fa-file-code-o' />
|
<i className='fa fa-file-code-o' />
|
||||||
<p>.md</p>
|
<p>.md</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
||||||
<i className='fa fa-file-text-o' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>.txt</p>
|
<p>.txt</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||||
<i className='fa fa-html5' />
|
<i className='fa fa-html5' />
|
||||||
<p>.html</p>
|
<p>.html</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--unable'>
|
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
|
||||||
<i className='fa fa-file-pdf-o' />
|
<i className='fa fa-file-pdf-o' />
|
||||||
<p>.pdf</p>
|
<p>.pdf</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -61,7 +61,8 @@ InfoPanelTrashed.propTypes = {
|
|||||||
createdAt: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
exportAsMd: PropTypes.func.isRequired,
|
exportAsMd: PropTypes.func.isRequired,
|
||||||
exportAsTxt: PropTypes.func.isRequired,
|
exportAsTxt: PropTypes.func.isRequired,
|
||||||
exportAsHtml: PropTypes.func.isRequired
|
exportAsHtml: PropTypes.func.isRequired,
|
||||||
|
exportAsPdf: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(InfoPanelTrashed, styles)
|
export default CSSModules(InfoPanelTrashed, styles)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import StarButton from './StarButton'
|
|||||||
import TagSelect from './TagSelect'
|
import TagSelect from './TagSelect'
|
||||||
import FolderSelect from './FolderSelect'
|
import FolderSelect from './FolderSelect'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import { hashHistory } from 'react-router'
|
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import markdown from 'browser/lib/markdownTextHelper'
|
import markdown from 'browser/lib/markdownTextHelper'
|
||||||
import StatusBar from '../StatusBar'
|
import StatusBar from '../StatusBar'
|
||||||
@@ -30,6 +29,8 @@ import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
|||||||
import striptags from 'striptags'
|
import striptags from 'striptags'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
import { replace } from 'connected-react-router'
|
||||||
|
|
||||||
class MarkdownNoteDetail extends React.Component {
|
class MarkdownNoteDetail extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -39,12 +40,15 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
isMovingNote: false,
|
isMovingNote: false,
|
||||||
note: Object.assign({
|
note: Object.assign({
|
||||||
title: '',
|
title: '',
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}, props.note),
|
}, props.note),
|
||||||
isLockButtonShown: false,
|
isLockButtonShown: props.config.editor.type !== 'SPLIT',
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
editorType: props.config.editor.type
|
editorType: props.config.editor.type,
|
||||||
|
switchPreview: props.config.editor.switchPreview
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchTimer = null
|
this.dispatchTimer = null
|
||||||
|
|
||||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||||
@@ -71,12 +75,26 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
|
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
|
||||||
if (this.saveQueue != null) this.saveNow()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
this.setState({
|
this.setState({
|
||||||
note: Object.assign({}, nextProps.note)
|
note: Object.assign({linesHighlighted: []}, nextProps.note)
|
||||||
}, () => {
|
}, () => {
|
||||||
this.refs.content.reload()
|
this.refs.content.reload()
|
||||||
if (this.refs.tags) this.refs.tags.reset()
|
if (this.refs.tags) this.refs.tags.reset()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Focus content if using blur or double click
|
||||||
|
// --> Moved here from componentDidMount so a re-render during search won't set focus to the editor
|
||||||
|
const {switchPreview} = nextProps.config.editor
|
||||||
|
|
||||||
|
if (this.state.switchPreview !== switchPreview) {
|
||||||
|
this.setState({
|
||||||
|
switchPreview
|
||||||
|
})
|
||||||
|
if (switchPreview === 'BLUR' || switchPreview === 'DBL_CLICK') {
|
||||||
|
console.log('setting focus', switchPreview)
|
||||||
|
this.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
@@ -94,7 +112,12 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
handleUpdateContent () {
|
handleUpdateContent () {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
note.content = this.refs.content.value
|
note.content = this.refs.content.value
|
||||||
note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
|
|
||||||
|
let title = findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)
|
||||||
|
title = striptags(title)
|
||||||
|
title = markdown.strip(title)
|
||||||
|
note.title = title
|
||||||
|
|
||||||
this.updateNote(note)
|
this.updateNote(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,12 +171,12 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
originNote: note,
|
originNote: note,
|
||||||
note: newNote
|
note: newNote
|
||||||
})
|
})
|
||||||
hashHistory.replace({
|
dispatch(replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: newNote.key
|
key: newNote.key
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
this.setState({
|
this.setState({
|
||||||
isMovingNote: false
|
isMovingNote: false
|
||||||
})
|
})
|
||||||
@@ -190,6 +213,40 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.emit('export:save-html')
|
ee.emit('export:save-html')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportAsPdf () {
|
||||||
|
ee.emit('export:save-pdf')
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown (e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
// tab key
|
||||||
|
case 9:
|
||||||
|
if (e.ctrlKey && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.jumpNextTab()
|
||||||
|
} else if (e.ctrlKey && e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.jumpPrevTab()
|
||||||
|
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.focusEditor()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
// I key
|
||||||
|
case 73:
|
||||||
|
{
|
||||||
|
const isSuper = global.process.platform === 'darwin'
|
||||||
|
? e.metaKey
|
||||||
|
: e.ctrlKey
|
||||||
|
if (isSuper) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.handleInfoButtonClick(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleTrashButtonClick (e) {
|
handleTrashButtonClick (e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
const { isTrashed } = note
|
const { isTrashed } = note
|
||||||
@@ -253,7 +310,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToggleLockButton () {
|
getToggleLockButton () {
|
||||||
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
|
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteKeyDown (e) {
|
handleDeleteKeyDown (e) {
|
||||||
@@ -262,7 +319,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleToggleLockButton (event, noteStatus) {
|
handleToggleLockButton (event, noteStatus) {
|
||||||
// first argument event is not used
|
// first argument event is not used
|
||||||
if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
|
if (noteStatus === 'CODE') {
|
||||||
this.setState({isLockButtonShown: true})
|
this.setState({isLockButtonShown: true})
|
||||||
} else {
|
} else {
|
||||||
this.setState({isLockButtonShown: false})
|
this.setState({isLockButtonShown: false})
|
||||||
@@ -288,7 +345,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSwitchMode (type) {
|
handleSwitchMode (type) {
|
||||||
this.setState({ editorType: type }, () => {
|
// If in split mode, hide the lock button
|
||||||
|
this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => {
|
||||||
this.focus()
|
this.focus()
|
||||||
const newConfig = Object.assign({}, this.props.config)
|
const newConfig = Object.assign({}, this.props.config)
|
||||||
newConfig.editor.type = type
|
newConfig.editor.type = type
|
||||||
@@ -331,7 +389,9 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
value={note.content}
|
value={note.content}
|
||||||
storageKey={note.storage}
|
storageKey={note.storage}
|
||||||
noteKey={note.key}
|
noteKey={note.key}
|
||||||
|
linesHighlighted={note.linesHighlighted}
|
||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
|
isLocked={this.state.isLocked}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
@@ -341,6 +401,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
value={note.content}
|
value={note.content}
|
||||||
storageKey={note.storage}
|
storageKey={note.storage}
|
||||||
noteKey={note.key}
|
noteKey={note.key}
|
||||||
|
linesHighlighted={note.linesHighlighted}
|
||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
/>
|
/>
|
||||||
@@ -348,7 +409,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { data, location, config } = this.props
|
const { data, dispatch, location, config } = this.props
|
||||||
const { note, editorType } = this.state
|
const { note, editorType } = this.state
|
||||||
const storageKey = note.storage
|
const storageKey = note.storage
|
||||||
const folderKey = note.folder
|
const folderKey = note.folder
|
||||||
@@ -381,13 +442,14 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
exportAsHtml={this.exportAsHtml}
|
exportAsHtml={this.exportAsHtml}
|
||||||
exportAsMd={this.exportAsMd}
|
exportAsMd={this.exportAsMd}
|
||||||
exportAsTxt={this.exportAsTxt}
|
exportAsTxt={this.exportAsTxt}
|
||||||
|
exportAsPdf={this.exportAsPdf}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
const detailTopBar = <div styleName='info'>
|
const detailTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<div styleName='info-left-top'>
|
<div>
|
||||||
<FolderSelect styleName='info-left-top-folderSelect'
|
<FolderSelect styleName='info-left-top-folderSelect'
|
||||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||||
ref='folder'
|
ref='folder'
|
||||||
@@ -402,12 +464,15 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||||
data={data}
|
data={data}
|
||||||
|
dispatch={dispatch}
|
||||||
onChange={this.handleUpdateTag.bind(this)}
|
onChange={this.handleUpdateTag.bind(this)}
|
||||||
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
<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}
|
||||||
@@ -420,7 +485,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
onFocus={(e) => this.handleFocus(e)}
|
onFocus={(e) => this.handleFocus(e)}
|
||||||
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src={imgSrc} />
|
<img src={imgSrc} />
|
||||||
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
|
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -440,13 +505,14 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.exportAsMd}
|
exportAsMd={this.exportAsMd}
|
||||||
exportAsTxt={this.exportAsTxt}
|
exportAsTxt={this.exportAsTxt}
|
||||||
exportAsHtml={this.exportAsHtml}
|
exportAsHtml={this.exportAsHtml}
|
||||||
wordCount={note.content.split(' ').length}
|
exportAsPdf={this.exportAsPdf}
|
||||||
|
wordCount={note.content.trim().split(/\s+/g).length}
|
||||||
letterCount={note.content.replace(/\r?\n/g, '').length}
|
letterCount={note.content.replace(/\r?\n/g, '').length}
|
||||||
type={note.type}
|
type={note.type}
|
||||||
print={this.print}
|
print={this.print}
|
||||||
@@ -458,6 +524,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
<div className='NoteDetail'
|
<div className='NoteDetail'
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
styleName='root'
|
styleName='root'
|
||||||
|
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||||
>
|
>
|
||||||
|
|
||||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||||
|
|||||||
@@ -80,4 +80,5 @@ body[data-theme="monokai"]
|
|||||||
body[data-theme="dracula"]
|
body[data-theme="dracula"]
|
||||||
.root
|
.root
|
||||||
border-left 1px solid $ui-dracula-borderColor
|
border-left 1px solid $ui-dracula-borderColor
|
||||||
background-color $ui-dracula-noteDetail-backgroundColor
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
|||||||
@@ -107,4 +107,12 @@ body[data-theme="monokai"]
|
|||||||
body[data-theme="dracula"]
|
body[data-theme="dracula"]
|
||||||
.info
|
.info
|
||||||
border-color $ui-dracula-borderColor
|
border-color $ui-dracula-borderColor
|
||||||
background-color $ui-dracula-noteDetail-backgroundColor
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
.info > div
|
||||||
|
> button
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
|
> img, span
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
@@ -10,7 +10,7 @@ const PermanentDeleteButton = ({
|
|||||||
<button styleName='control-trashButton--in-trash'
|
<button styleName='control-trashButton--in-trash'
|
||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img src='../resources/icon/icon-trash.svg' />
|
||||||
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import StarButton from './StarButton'
|
|||||||
import TagSelect from './TagSelect'
|
import TagSelect from './TagSelect'
|
||||||
import FolderSelect from './FolderSelect'
|
import FolderSelect from './FolderSelect'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import {hashHistory} from 'react-router'
|
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
import 'codemirror-mode-elixir'
|
import 'codemirror-mode-elixir'
|
||||||
@@ -18,8 +17,8 @@ import context from 'browser/lib/context'
|
|||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
||||||
import convertModeName from 'browser/lib/convertModeName'
|
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import FullscreenButton from './FullscreenButton'
|
||||||
import TrashButton from './TrashButton'
|
import TrashButton from './TrashButton'
|
||||||
import RestoreButton from './RestoreButton'
|
import RestoreButton from './RestoreButton'
|
||||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||||
@@ -30,6 +29,8 @@ import { formatDate } from 'browser/lib/date-formatter'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
import { replace } from 'connected-react-router'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
@@ -48,7 +49,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
note: Object.assign({
|
note: Object.assign({
|
||||||
description: ''
|
description: ''
|
||||||
}, props.note, {
|
}, props.note, {
|
||||||
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +77,9 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const nextNote = Object.assign({
|
const nextNote = Object.assign({
|
||||||
description: ''
|
description: ''
|
||||||
}, nextProps.note, {
|
}, nextProps.note, {
|
||||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
|
snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
snippetIndex: 0,
|
snippetIndex: 0,
|
||||||
note: nextNote
|
note: nextNote
|
||||||
@@ -164,12 +166,12 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
originNote: note,
|
originNote: note,
|
||||||
note: newNote
|
note: newNote
|
||||||
})
|
})
|
||||||
hashHistory.replace({
|
dispatch(replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: newNote.key
|
key: newNote.key
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
this.setState({
|
this.setState({
|
||||||
isMovingNote: false
|
isMovingNote: false
|
||||||
})
|
})
|
||||||
@@ -410,6 +412,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
return (e) => {
|
return (e) => {
|
||||||
const snippets = this.state.note.snippets.slice()
|
const snippets = this.state.note.snippets.slice()
|
||||||
snippets[index].content = this.refs['code-' + index].value
|
snippets[index].content = this.refs['code-' + index].value
|
||||||
|
snippets[index].linesHighlighted = e.options.linesHighlighted
|
||||||
|
|
||||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||||
this.setState(state => ({
|
this.setState(state => ({
|
||||||
note: state.note
|
note: state.note
|
||||||
@@ -434,6 +438,18 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
// I key
|
||||||
|
case 73:
|
||||||
|
{
|
||||||
|
const isSuper = global.process.platform === 'darwin'
|
||||||
|
? e.metaKey
|
||||||
|
: e.ctrlKey
|
||||||
|
if (isSuper) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.handleInfoButtonClick(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
// L key
|
// L key
|
||||||
case 76:
|
case 76:
|
||||||
{
|
{
|
||||||
@@ -502,6 +518,19 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleWrapLineButtonClick (e) {
|
||||||
|
context.popup([
|
||||||
|
{
|
||||||
|
label: 'on',
|
||||||
|
click: (e) => this.handleWrapLineItemClick(e, true)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'off',
|
||||||
|
click: (e) => this.handleWrapLineItemClick(e, false)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
handleIndentSizeItemClick (e, indentSize) {
|
handleIndentSizeItemClick (e, indentSize) {
|
||||||
const { config, dispatch } = this.props
|
const { config, dispatch } = this.props
|
||||||
const editor = Object.assign({}, config.editor, {
|
const editor = Object.assign({}, config.editor, {
|
||||||
@@ -534,6 +563,22 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleWrapLineItemClick (e, lineWrapping) {
|
||||||
|
const { config, dispatch } = this.props
|
||||||
|
const editor = Object.assign({}, config.editor, {
|
||||||
|
lineWrapping
|
||||||
|
})
|
||||||
|
ConfigManager.set({
|
||||||
|
editor
|
||||||
|
})
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config: {
|
||||||
|
editor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
this.refs.description.focus()
|
this.refs.description.focus()
|
||||||
}
|
}
|
||||||
@@ -584,13 +629,16 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSnippet () {
|
addSnippet () {
|
||||||
const { config } = this.props
|
const { config: { editor: { snippetDefaultLanguage } } } = this.props
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
|
const defaultLanguage = snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
|
||||||
|
|
||||||
note.snippets = note.snippets.concat([{
|
note.snippets = note.snippets.concat([{
|
||||||
name: '',
|
name: '',
|
||||||
mode: config.editor.snippetDefaultLanguage || 'text',
|
mode: defaultLanguage,
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}])
|
}])
|
||||||
const snippetIndex = note.snippets.length - 1
|
const snippetIndex = note.snippets.length - 1
|
||||||
|
|
||||||
@@ -633,22 +681,32 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
showWarning () {
|
showWarning (e, msg) {
|
||||||
|
const warningMessage = (msg) => ({
|
||||||
|
'export-txt': 'Text export',
|
||||||
|
'export-md': 'Markdown export',
|
||||||
|
'export-html': 'HTML export',
|
||||||
|
'export-pdf': 'PDF export',
|
||||||
|
'print': 'Print'
|
||||||
|
})[msg]
|
||||||
|
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Sorry!'),
|
message: i18n.__('Sorry!'),
|
||||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
||||||
buttons: [i18n.__('OK')]
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { data, config, location } = this.props
|
const { data, dispatch, config, location } = this.props
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
const storageKey = note.storage
|
const storageKey = note.storage
|
||||||
const folderKey = note.folder
|
const folderKey = note.folder
|
||||||
|
|
||||||
|
const autoDetect = config.editor.snippetDefaultLanguage === 'Auto Detect'
|
||||||
|
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
@@ -673,10 +731,6 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
const viewList = note.snippets.map((snippet, index) => {
|
const viewList = note.snippets.map((snippet, index) => {
|
||||||
const isActive = this.state.snippetIndex === index
|
const isActive = this.state.snippetIndex === index
|
||||||
|
|
||||||
let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
|
|
||||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
|
||||||
|
|
||||||
return <div styleName='tabView'
|
return <div styleName='tabView'
|
||||||
key={index}
|
key={index}
|
||||||
style={{zIndex: isActive ? 5 : 4}}
|
style={{zIndex: isActive ? 5 : 4}}
|
||||||
@@ -685,26 +739,35 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
? <MarkdownEditor styleName='tabView-content'
|
? <MarkdownEditor styleName='tabView-content'
|
||||||
value={snippet.content}
|
value={snippet.content}
|
||||||
config={config}
|
config={config}
|
||||||
|
linesHighlighted={snippet.linesHighlighted}
|
||||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||||
ref={'code-' + index}
|
ref={'code-' + index}
|
||||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
/>
|
/>
|
||||||
: <CodeEditor styleName='tabView-content'
|
: <CodeEditor styleName='tabView-content'
|
||||||
mode={snippet.mode}
|
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
|
||||||
value={snippet.content}
|
value={snippet.content}
|
||||||
|
linesHighlighted={snippet.linesHighlighted}
|
||||||
|
lineWrapping={config.editor.lineWrapping}
|
||||||
theme={config.editor.theme}
|
theme={config.editor.theme}
|
||||||
fontFamily={config.editor.fontFamily}
|
fontFamily={config.editor.fontFamily}
|
||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
matchingPairs={config.editor.matchingPairs}
|
||||||
|
matchingTriples={config.editor.matchingTriples}
|
||||||
|
explodingPairs={config.editor.explodingPairs}
|
||||||
keyMap={config.editor.keyMap}
|
keyMap={config.editor.keyMap}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
enableTableEditor={config.editor.enableTableEditor}
|
enableTableEditor={config.editor.enableTableEditor}
|
||||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||||
ref={'code-' + index}
|
ref={'code-' + index}
|
||||||
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
|
hotkey={config.hotkey}
|
||||||
|
autoDetect={autoDetect}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -738,13 +801,14 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
exportAsTxt={this.showWarning}
|
exportAsTxt={this.showWarning}
|
||||||
exportAsHtml={this.showWarning}
|
exportAsHtml={this.showWarning}
|
||||||
|
exportAsPdf={this.showWarning}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
const detailTopBar = <div styleName='info'>
|
const detailTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<div styleName='info-left-top'>
|
<div>
|
||||||
<FolderSelect styleName='info-left-top-folderSelect'
|
<FolderSelect styleName='info-left-top-folderSelect'
|
||||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||||
ref='folder'
|
ref='folder'
|
||||||
@@ -759,7 +823,9 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||||
data={data}
|
data={data}
|
||||||
|
dispatch={dispatch}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
@@ -768,11 +834,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
|
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
|
||||||
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
|
||||||
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
|
|
||||||
@@ -783,12 +845,15 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
exportAsTxt={this.showWarning}
|
exportAsTxt={this.showWarning}
|
||||||
|
exportAsHtml={this.showWarning}
|
||||||
|
exportAsPdf={this.showWarning}
|
||||||
type={note.type}
|
type={note.type}
|
||||||
|
print={this.showWarning}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -865,6 +930,12 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
size: {config.editor.indentSize}
|
size: {config.editor.indentSize}
|
||||||
<i className='fa fa-caret-down' />
|
<i className='fa fa-caret-down' />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => this.handleWrapLineButtonClick(e)}
|
||||||
|
>
|
||||||
|
Wrap Line: {config.editor.lineWrapping ? 'on' : 'off'}
|
||||||
|
<i className='fa fa-caret-down' />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusBar
|
<StatusBar
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
absolute left right
|
absolute left right
|
||||||
top 55px
|
top 70px
|
||||||
height 30px
|
height 30px
|
||||||
display flex
|
display flex
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
@@ -57,6 +57,9 @@
|
|||||||
.tabList .tabButton
|
.tabList .tabButton
|
||||||
navWhiteButtonColor()
|
navWhiteButtonColor()
|
||||||
width 30px
|
width 30px
|
||||||
|
border-left 1px solid $ui-borderColor
|
||||||
|
border-top 1px solid $ui-borderColor
|
||||||
|
border-right 1px solid $ui-borderColor
|
||||||
|
|
||||||
.tabView
|
.tabView
|
||||||
absolute left right bottom
|
absolute left right bottom
|
||||||
@@ -98,17 +101,34 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"], body[data-theme="default"]
|
||||||
.root
|
.root
|
||||||
box-shadow $note-detail-box-shadow
|
box-shadow $note-detail-box-shadow
|
||||||
border none
|
border none
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||||
|
color $ui-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
border-left 1px solid $ui-dark-borderColor
|
border-left 1px solid $ui-dark-borderColor
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
box-shadow none
|
box-shadow none
|
||||||
|
|
||||||
|
.tabList .tabButton
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
color $ui-dark-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
.body
|
.body
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
@@ -118,7 +138,6 @@ body[data-theme="dark"]
|
|||||||
border 1px solid $ui-dark-borderColor
|
border 1px solid $ui-dark-borderColor
|
||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
.tabList .list
|
.tabList .list
|
||||||
@@ -150,6 +169,15 @@ body[data-theme="solarized-dark"]
|
|||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
border 1px solid $ui-solarized-dark-borderColor
|
border 1px solid $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
.tabList .tabButton
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
@@ -167,6 +195,14 @@ body[data-theme="monokai"]
|
|||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
border 1px solid $ui-monokai-borderColor
|
border 1px solid $ui-monokai-borderColor
|
||||||
|
|
||||||
|
.tabList .tabButton
|
||||||
|
border-color $ui-monokai-borderColor
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
background-color $ui-monokai-noteDetail-backgroundColor
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
@@ -184,6 +220,14 @@ body[data-theme="dracula"]
|
|||||||
color $ui-dracula-text-color
|
color $ui-dracula-text-color
|
||||||
border 1px solid $ui-dracula-borderColor
|
border 1px solid $ui-dracula-borderColor
|
||||||
|
|
||||||
|
.tabList .tabButton
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
background-color $ui-dracula-noteDetail-backgroundColor
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
color $ui-dracula-text-color
|
color $ui-dracula-text-color
|
||||||
@@ -54,7 +54,7 @@ class StarButton extends React.Component {
|
|||||||
: '../resources/icon/icon-star.svg'
|
: '../resources/icon/icon-star.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span styleName='tooltip'>{i18n.__('Star')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Star')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
|
.tooltip:lang(ja)
|
||||||
|
@extend .tooltip
|
||||||
|
right 103px
|
||||||
|
width 70px
|
||||||
|
|
||||||
.root--active
|
.root--active
|
||||||
@extend .root
|
@extend .root
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
@@ -37,4 +42,4 @@ body[data-theme="dark"]
|
|||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
&:hover
|
&:hover
|
||||||
transition 0.2s
|
transition 0.2s
|
||||||
color alpha($ui-favorite-star-button-color, 0.6)
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import invertColor from 'invert-color'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TagSelect.styl'
|
import styles from './TagSelect.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
@@ -7,6 +8,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import Autosuggest from 'react-autosuggest'
|
import Autosuggest from 'react-autosuggest'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
class TagSelect extends React.Component {
|
class TagSelect extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -45,8 +47,14 @@ class TagSelect extends React.Component {
|
|||||||
value = _.isArray(value)
|
value = _.isArray(value)
|
||||||
? value.slice()
|
? value.slice()
|
||||||
: []
|
: []
|
||||||
value.push(newTag)
|
|
||||||
value = _.uniq(value)
|
if (!_.includes(value, newTag)) {
|
||||||
|
value.push(newTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.saveTagsAlphabetically) {
|
||||||
|
value = _.sortBy(value)
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
newTag: ''
|
newTag: ''
|
||||||
@@ -89,8 +97,11 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTagLabelClick (tag) {
|
handleTagLabelClick (tag) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push(`/tags/${tag}`)
|
|
||||||
|
// Note: `tag` requires encoding later.
|
||||||
|
// E.g. % in tag is a problem (see issue #3170) - encodeURIComponent(tag) is not working.
|
||||||
|
dispatch(push(`/tags/${tag}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagRemoveButtonClick (tag) {
|
handleTagRemoveButtonClick (tag) {
|
||||||
@@ -179,19 +190,34 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { value, className, showTagsAlphabetically } = this.props
|
const { value, className, showTagsAlphabetically, coloredTags } = this.props
|
||||||
|
|
||||||
const tagList = _.isArray(value)
|
const tagList = _.isArray(value)
|
||||||
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
|
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
|
||||||
|
const wrapperStyle = {}
|
||||||
|
const textStyle = {}
|
||||||
|
const BLACK = '#333333'
|
||||||
|
const WHITE = '#f1f1f1'
|
||||||
|
const color = coloredTags[tag]
|
||||||
|
const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE })
|
||||||
|
let iconRemove = '../resources/icon/icon-x.svg'
|
||||||
|
if (color) {
|
||||||
|
wrapperStyle.backgroundColor = color
|
||||||
|
textStyle.color = invertedColor
|
||||||
|
}
|
||||||
|
if (invertedColor === WHITE) {
|
||||||
|
iconRemove = '../resources/icon/icon-x-light.svg'
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span styleName='tag'
|
<span styleName='tag'
|
||||||
key={tag}
|
key={tag}
|
||||||
|
style={wrapperStyle}
|
||||||
>
|
>
|
||||||
<span styleName='tag-label' onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
|
<span styleName='tag-label' style={textStyle} onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
|
||||||
<button styleName='tag-removeButton'
|
<button styleName='tag-removeButton'
|
||||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
|
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
|
||||||
>
|
>
|
||||||
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
|
<img className='tag-removeButton-icon' src={iconRemove} width='8px' />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -233,14 +259,12 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TagSelect.contextTypes = {
|
|
||||||
router: PropTypes.shape({})
|
|
||||||
}
|
|
||||||
|
|
||||||
TagSelect.propTypes = {
|
TagSelect.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
value: PropTypes.arrayOf(PropTypes.string),
|
value: PropTypes.arrayOf(PropTypes.string),
|
||||||
onChange: PropTypes.func
|
onChange: PropTypes.func,
|
||||||
|
coloredTags: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(TagSelect, styles)
|
export default CSSModules(TagSelect, styles)
|
||||||
|
|||||||
@@ -3,19 +3,18 @@
|
|||||||
align-items center
|
align-items center
|
||||||
user-select none
|
user-select none
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
width 100%
|
width 96%
|
||||||
overflow-x scroll
|
overflow-x auto
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
margin-top 31px
|
top 50px
|
||||||
position absolute
|
position absolute
|
||||||
|
&::-webkit-scrollbar
|
||||||
.root::-webkit-scrollbar
|
height 8px
|
||||||
display none
|
|
||||||
|
|
||||||
.tag
|
.tag
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
margin 0px 2px
|
margin 0px 2px 2px
|
||||||
padding 2px 4px
|
padding 2px 4px
|
||||||
background-color alpha($ui-tag-backgroundColor, 3%)
|
background-color alpha($ui-tag-backgroundColor, 3%)
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
|
|||||||
@@ -8,19 +8,19 @@ const ToggleModeButton = ({
|
|||||||
onClick, editorType
|
onClick, editorType
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName='control-toggleModeButton'>
|
<div styleName='control-toggleModeButton'>
|
||||||
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
<div styleName={editorType === 'SPLIT' ? 'active' : undefined} onClick={() => onClick('SPLIT')}>
|
||||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
<img src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
<img src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||||
</div>
|
</div>
|
||||||
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
ToggleModeButton.propTypes = {
|
ToggleModeButton.propTypes = {
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
editorType: PropTypes.string.Required
|
editorType: PropTypes.string.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(ToggleModeButton, styles)
|
export default CSSModules(ToggleModeButton, styles)
|
||||||
|
|||||||
@@ -40,6 +40,11 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
|
.tooltip:lang(ja)
|
||||||
|
@extend .tooltip
|
||||||
|
left -8px
|
||||||
|
width 70px
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
@@ -70,3 +75,10 @@ body[data-theme="dracula"]
|
|||||||
.active
|
.active
|
||||||
background-color #bd93f9
|
background-color #bd93f9
|
||||||
box-shadow 2px 0px 7px #222222
|
box-shadow 2px 0px 7px #222222
|
||||||
|
|
||||||
|
.control-toggleModeButton
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
|
> div img
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ const TrashButton = ({
|
|||||||
<button styleName='control-trashButton'
|
<button styleName='control-trashButton'
|
||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img src='../resources/icon/icon-trash.svg' />
|
||||||
<span styleName='tooltip'>{i18n.__('Trash')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
|
.tooltip:lang(ja)
|
||||||
|
@extend .tooltip
|
||||||
|
right 46px
|
||||||
|
|
||||||
.control-trashButton--in-trash
|
.control-trashButton--in-trash
|
||||||
top 60px
|
top 60px
|
||||||
topBarButtonRight()
|
topBarButtonRight()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import StatusBar from '../StatusBar'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import debounceRender from 'react-debounce-render'
|
import debounceRender from 'react-debounce-render'
|
||||||
import searchFromNotes from 'browser/lib/search'
|
import searchFromNotes from 'browser/lib/search'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
@@ -36,11 +37,11 @@ class Detail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { location, data, params, config } = this.props
|
const { location, data, match: { params }, config } = this.props
|
||||||
|
const noteKey = location.search !== '' && queryString.parse(location.search).key
|
||||||
let note = null
|
let note = null
|
||||||
|
|
||||||
if (location.query.key != null) {
|
if (location.search !== '') {
|
||||||
const noteKey = location.query.key
|
|
||||||
const allNotes = data.noteMap.map(note => note)
|
const allNotes = data.noteMap.map(note => note)
|
||||||
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
|
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||||
let displayedNotes = allNotes
|
let displayedNotes = allNotes
|
||||||
@@ -49,16 +50,14 @@ class Detail extends React.Component {
|
|||||||
const searchStr = params.searchword
|
const searchStr = params.searchword
|
||||||
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
|
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
|
||||||
: searchFromNotes(allNotes, searchStr)
|
: searchFromNotes(allNotes, searchStr)
|
||||||
}
|
} else if (location.pathname.match(/^\/tags/)) {
|
||||||
|
|
||||||
if (location.pathname.match(/\/tags/)) {
|
|
||||||
const listOfTags = params.tagname.split(' ')
|
const listOfTags = params.tagname.split(' ')
|
||||||
displayedNotes = data.noteMap.map(note => note).filter(note =>
|
displayedNotes = data.noteMap.map(note => note).filter(note =>
|
||||||
listOfTags.every(tag => note.tags.includes(tag))
|
listOfTags.every(tag => note.tags.includes(tag))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname.match(/\/trashed/)) {
|
if (location.pathname.match(/^\/trashed/)) {
|
||||||
displayedNotes = trashedNotes
|
displayedNotes = trashedNotes
|
||||||
} else {
|
} else {
|
||||||
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
|
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
|
||||||
|
|||||||
16
browser/main/DevTools/index.dev.js
Normal file
16
browser/main/DevTools/index.dev.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { createDevTools } from 'redux-devtools'
|
||||||
|
import LogMonitor from 'redux-devtools-log-monitor'
|
||||||
|
import DockMonitor from 'redux-devtools-dock-monitor'
|
||||||
|
|
||||||
|
const DevTools = createDevTools(
|
||||||
|
<DockMonitor
|
||||||
|
toggleVisibilityKey='ctrl-h'
|
||||||
|
changePositionKey='ctrl-q'
|
||||||
|
defaultIsVisible={false}
|
||||||
|
>
|
||||||
|
<LogMonitor theme='tomorrow' />
|
||||||
|
</DockMonitor>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default DevTools
|
||||||
8
browser/main/DevTools/index.js
Normal file
8
browser/main/DevTools/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
module.exports = require('./index.dev').default
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
module.exports = require('./index.prod').default
|
||||||
|
}
|
||||||
6
browser/main/DevTools/index.prod.js
Normal file
6
browser/main/DevTools/index.prod.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const DevTools = () => <div />
|
||||||
|
DevTools.instrument = () => {}
|
||||||
|
|
||||||
|
export default DevTools
|
||||||
@@ -12,12 +12,12 @@ import _ from 'lodash'
|
|||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import { hashHistory } from 'react-router'
|
import { store } from 'browser/main/store'
|
||||||
import store from 'browser/main/store'
|
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import { getLocales } from 'browser/lib/Languages'
|
import { getLocales } from 'browser/lib/Languages'
|
||||||
import applyShortcuts from 'browser/main/lib/shortcutManager'
|
import applyShortcuts from 'browser/main/lib/shortcutManager'
|
||||||
import {chooseTheme, applyTheme} from 'browser/main/lib/ThemeManager'
|
import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
@@ -97,12 +97,14 @@ class Main extends React.Component {
|
|||||||
{
|
{
|
||||||
name: 'example.html',
|
name: 'example.html',
|
||||||
mode: 'html',
|
mode: 'html',
|
||||||
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
|
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
|
||||||
|
linesHighlighted: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'example.js',
|
name: 'example.js',
|
||||||
mode: 'javascript',
|
mode: 'javascript',
|
||||||
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
|
content: "var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)",
|
||||||
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -131,7 +133,7 @@ class Main extends React.Component {
|
|||||||
.then(() => data.storage)
|
.then(() => data.storage)
|
||||||
})
|
})
|
||||||
.then(storage => {
|
.then(storage => {
|
||||||
hashHistory.push('/storages/' + storage.key)
|
store.dispatch(push('/storages/' + storage.key))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
throw err
|
throw err
|
||||||
@@ -167,13 +169,25 @@ class Main extends React.Component {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
||||||
|
|
||||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||||
|
eventEmitter.on('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
|
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
|
||||||
|
eventEmitter.off('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMenuBarVisible () {
|
||||||
|
const { config } = this.props
|
||||||
|
const { ui } = config
|
||||||
|
|
||||||
|
const newUI = Object.assign(ui, {showMenuBar: !ui.showMenuBar})
|
||||||
|
const newConfig = Object.assign(config, newUI)
|
||||||
|
ConfigManager.set(newConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLeftSlideMouseDown (e) {
|
handleLeftSlideMouseDown (e) {
|
||||||
@@ -234,8 +248,8 @@ class Main extends React.Component {
|
|||||||
if (this.state.isRightSliderFocused) {
|
if (this.state.isRightSliderFocused) {
|
||||||
const offset = this.refs.body.getBoundingClientRect().left
|
const offset = this.refs.body.getBoundingClientRect().left
|
||||||
let newListWidth = e.pageX - offset
|
let newListWidth = e.pageX - offset
|
||||||
if (newListWidth < 10) {
|
if (newListWidth < 180) {
|
||||||
newListWidth = 10
|
newListWidth = 180
|
||||||
} else if (newListWidth > 600) {
|
} else if (newListWidth > 600) {
|
||||||
newListWidth = 600
|
newListWidth = 600
|
||||||
}
|
}
|
||||||
@@ -298,7 +312,7 @@ class Main extends React.Component {
|
|||||||
onMouseUp={e => this.handleMouseUp(e)}
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
>
|
>
|
||||||
<SideNav
|
<SideNav
|
||||||
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
|
{..._.pick(this.props, ['dispatch', 'data', 'config', 'match', 'location'])}
|
||||||
width={this.state.navWidth}
|
width={this.state.navWidth}
|
||||||
/>
|
/>
|
||||||
{!config.isSideNavFolded &&
|
{!config.isSideNavFolded &&
|
||||||
@@ -328,7 +342,7 @@ class Main extends React.Component {
|
|||||||
'dispatch',
|
'dispatch',
|
||||||
'config',
|
'config',
|
||||||
'data',
|
'data',
|
||||||
'params',
|
'match',
|
||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
@@ -338,7 +352,7 @@ class Main extends React.Component {
|
|||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
'config',
|
'config',
|
||||||
'params',
|
'match',
|
||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
@@ -360,7 +374,7 @@ class Main extends React.Component {
|
|||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
'config',
|
'config',
|
||||||
'params',
|
'match',
|
||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
||||||
|
|||||||
@@ -21,42 +21,39 @@ class NewNoteButton extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.newNoteHandler = () => {
|
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
|
||||||
this.handleNewNoteButtonClick()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
eventEmitter.on('top:new-note', this.newNoteHandler)
|
eventEmitter.on('top:new-note', this.handleNewNoteButtonClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
eventEmitter.off('top:new-note', this.newNoteHandler)
|
eventEmitter.off('top:new-note', this.handleNewNoteButtonClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewNoteButtonClick (e) {
|
handleNewNoteButtonClick (e) {
|
||||||
const { location, dispatch, config } = this.props
|
const { location, dispatch, match: { params }, config } = this.props
|
||||||
const { storage, folder } = this.resolveTargetFolder()
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
|
|
||||||
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||||
createMarkdownNote(storage.key, folder.key, dispatch, location)
|
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
||||||
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||||
createSnippetNote(storage.key, folder.key, dispatch, location, config)
|
createSnippetNote(storage.key, folder.key, dispatch, location, params, config)
|
||||||
} else {
|
} else {
|
||||||
modal.open(NewNoteModal, {
|
modal.open(NewNoteModal, {
|
||||||
storage: storage.key,
|
storage: storage.key,
|
||||||
folder: folder.key,
|
folder: folder.key,
|
||||||
dispatch,
|
dispatch,
|
||||||
location,
|
location,
|
||||||
|
params,
|
||||||
config
|
config
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveTargetFolder () {
|
resolveTargetFolder () {
|
||||||
const { data, params } = this.props
|
const { data, match: { params } } = this.props
|
||||||
let storage = data.storageMap.get(params.storageKey)
|
let storage = data.storageMap.get(params.storageKey)
|
||||||
|
|
||||||
// Find first storage
|
// Find first storage
|
||||||
if (storage == null) {
|
if (storage == null) {
|
||||||
for (const kv of data.storageMap) {
|
for (const kv of data.storageMap) {
|
||||||
@@ -92,8 +89,8 @@ class NewNoteButton extends React.Component {
|
|||||||
>
|
>
|
||||||
<div styleName='control'>
|
<div styleName='control'>
|
||||||
<button styleName='control-newNoteButton'
|
<button styleName='control-newNoteButton'
|
||||||
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
onClick={this.handleNewNoteButtonClick}>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
|
<img src='../resources/icon/icon-newnote.svg' />
|
||||||
<span styleName='control-newNoteButton-tooltip'>
|
<span styleName='control-newNoteButton-tooltip'>
|
||||||
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
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'
|
||||||
import debounceRender from 'react-debounce-render'
|
|
||||||
import styles from './NoteList.styl'
|
import styles from './NoteList.styl'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
@@ -15,23 +14,42 @@ import NoteItemSimple from 'browser/components/NoteItemSimple'
|
|||||||
import searchFromNotes from 'browser/lib/search'
|
import searchFromNotes from 'browser/lib/search'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { hashHistory } from 'react-router'
|
import { push, replace } from 'connected-react-router'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import Markdown from '../../lib/markdown'
|
import Markdown from '../../lib/markdown'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
const WP_POST_PATH = '/wp/v2/posts'
|
const WP_POST_PATH = '/wp/v2/posts'
|
||||||
|
|
||||||
|
const regexMatchStartingTitleNumber = new RegExp('^([0-9]*\.?[0-9]+).*$')
|
||||||
|
|
||||||
function sortByCreatedAt (a, b) {
|
function sortByCreatedAt (a, b) {
|
||||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByAlphabetical (a, b) {
|
function sortByAlphabetical (a, b) {
|
||||||
|
const matchA = regexMatchStartingTitleNumber.exec(a.title)
|
||||||
|
const matchB = regexMatchStartingTitleNumber.exec(b.title)
|
||||||
|
|
||||||
|
if (matchA && matchA.length === 2 && matchB && matchB.length === 2) {
|
||||||
|
// Both note titles are starting with a float. We will compare it now.
|
||||||
|
const floatA = parseFloat(matchA[1])
|
||||||
|
const floatB = parseFloat(matchB[1])
|
||||||
|
|
||||||
|
const diff = floatA - floatB
|
||||||
|
if (diff !== 0) {
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
// The float values are equal. We will compare the full title.
|
||||||
|
}
|
||||||
|
|
||||||
return a.title.localeCompare(b.title)
|
return a.title.localeCompare(b.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,13 +82,15 @@ class NoteList extends React.Component {
|
|||||||
this.focusHandler = () => {
|
this.focusHandler = () => {
|
||||||
this.refs.list.focus()
|
this.refs.list.focus()
|
||||||
}
|
}
|
||||||
this.alertIfSnippetHandler = () => {
|
this.alertIfSnippetHandler = (event, msg) => {
|
||||||
this.alertIfSnippet()
|
this.alertIfSnippet(msg)
|
||||||
}
|
}
|
||||||
this.importFromFileHandler = this.importFromFile.bind(this)
|
this.importFromFileHandler = this.importFromFile.bind(this)
|
||||||
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
||||||
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
||||||
|
this.handleNoteListBlur = this.handleNoteListBlur.bind(this)
|
||||||
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
||||||
|
this.cloneNote = this.cloneNote.bind(this)
|
||||||
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)
|
||||||
@@ -96,6 +116,7 @@ class NoteList extends React.Component {
|
|||||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||||
ee.on('list:next', this.selectNextNoteHandler)
|
ee.on('list:next', this.selectNextNoteHandler)
|
||||||
ee.on('list:prior', this.selectPriorNoteHandler)
|
ee.on('list:prior', this.selectPriorNoteHandler)
|
||||||
|
ee.on('list:clone', this.cloneNote)
|
||||||
ee.on('list:focus', this.focusHandler)
|
ee.on('list:focus', this.focusHandler)
|
||||||
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
|
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||||
ee.on('import:file', this.importFromFileHandler)
|
ee.on('import:file', this.importFromFileHandler)
|
||||||
@@ -118,6 +139,7 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
ee.off('list:next', this.selectNextNoteHandler)
|
ee.off('list:next', this.selectNextNoteHandler)
|
||||||
ee.off('list:prior', this.selectPriorNoteHandler)
|
ee.off('list:prior', this.selectPriorNoteHandler)
|
||||||
|
ee.off('list:clone', this.cloneNote)
|
||||||
ee.off('list:focus', this.focusHandler)
|
ee.off('list:focus', this.focusHandler)
|
||||||
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
|
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||||
ee.off('import:file', this.importFromFileHandler)
|
ee.off('import:file', this.importFromFileHandler)
|
||||||
@@ -125,15 +147,15 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props
|
const { dispatch, location } = this.props
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const visibleNoteKeys = this.notes.map(note => note.key)
|
const visibleNoteKeys = this.notes && this.notes.map(note => note.key)
|
||||||
const note = this.notes[0]
|
const note = this.notes && this.notes[0]
|
||||||
const prevKey = prevProps.location.query.key
|
const key = location.search && queryString.parse(location.search).key
|
||||||
|
const prevKey = prevProps.location.search && queryString.parse(prevProps.location.search).key
|
||||||
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
||||||
|
|
||||||
if (note && location.query.key == null) {
|
if (note && location.search === '') {
|
||||||
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
|
// A visible note is an active note
|
||||||
@@ -143,17 +165,17 @@ class NoteList extends React.Component {
|
|||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
|
|
||||||
router.replace({
|
dispatch(replace({ // was passed with context - we can use connected router here
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: noteKey
|
key: noteKey
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto scroll
|
// Auto scroll
|
||||||
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
|
if (_.isString(key) && prevKey === key) {
|
||||||
const targetIndex = this.getTargetIndex()
|
const targetIndex = this.getTargetIndex()
|
||||||
if (targetIndex > -1) {
|
if (targetIndex > -1) {
|
||||||
const list = this.refs.list
|
const list = this.refs.list
|
||||||
@@ -173,20 +195,19 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focusNote (selectedNoteKeys, noteKey) {
|
focusNote (selectedNoteKeys, noteKey, pathname) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
const { location } = this.props
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedNoteKeys
|
selectedNoteKeys
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push({
|
dispatch(push({
|
||||||
pathname: location.pathname,
|
pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: noteKey
|
key: noteKey
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteKeyFromTargetIndex (targetIndex) {
|
getNoteKeyFromTargetIndex (targetIndex) {
|
||||||
@@ -201,6 +222,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
let { selectedNoteKeys } = this.state
|
let { selectedNoteKeys } = this.state
|
||||||
const { shiftKeyDown } = this.state
|
const { shiftKeyDown } = this.state
|
||||||
|
const { location } = this.props
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
let targetIndex = this.getTargetIndex()
|
||||||
|
|
||||||
@@ -217,7 +239,7 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys.push(priorNoteKey)
|
selectedNoteKeys.push(priorNoteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focusNote(selectedNoteKeys, priorNoteKey)
|
this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
@@ -228,6 +250,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
let { selectedNoteKeys } = this.state
|
let { selectedNoteKeys } = this.state
|
||||||
const { shiftKeyDown } = this.state
|
const { shiftKeyDown } = this.state
|
||||||
|
const { location } = this.props
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
let targetIndex = this.getTargetIndex()
|
||||||
const isTargetLastNote = targetIndex === this.notes.length - 1
|
const isTargetLastNote = targetIndex === this.notes.length - 1
|
||||||
@@ -250,19 +273,28 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys.push(nextNoteKey)
|
selectedNoteKeys.push(nextNoteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focusNote(selectedNoteKeys, nextNoteKey)
|
this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
|
|
||||||
jumpNoteByHashHandler (event, noteHash) {
|
jumpNoteByHashHandler (event, noteHash) {
|
||||||
|
const { data } = this.props
|
||||||
|
|
||||||
// first argument event isn't used.
|
// first argument event isn't used.
|
||||||
if (this.notes === null || this.notes.length === 0) {
|
if (this.notes === null || this.notes.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedNoteKeys = [noteHash]
|
const selectedNoteKeys = [noteHash]
|
||||||
this.focusNote(selectedNoteKeys, noteHash)
|
|
||||||
|
let locationToSelect = '/home'
|
||||||
|
const noteByHash = data.noteMap.map((note) => note).find(note => note.key === noteHash)
|
||||||
|
if (noteByHash !== undefined) {
|
||||||
|
locationToSelect = '/storages/' + noteByHash.storage + '/folders/' + noteByHash.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
this.focusNote(selectedNoteKeys, noteHash, locationToSelect)
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
@@ -276,12 +308,6 @@ class NoteList extends React.Component {
|
|||||||
ee.emit('top:new-note')
|
ee.emit('top:new-note')
|
||||||
}
|
}
|
||||||
|
|
||||||
// D key
|
|
||||||
if (e.keyCode === 68) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.deleteNote()
|
|
||||||
}
|
|
||||||
|
|
||||||
// E key
|
// E key
|
||||||
if (e.keyCode === 69) {
|
if (e.keyCode === 69) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -323,9 +349,15 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotes () {
|
handleNoteListBlur () {
|
||||||
const { data, params, location } = this.props
|
this.setState({
|
||||||
|
shiftKeyDown: false,
|
||||||
|
ctrlKeyDown: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotes () {
|
||||||
|
const { data, match: { params }, location } = this.props
|
||||||
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
|
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
|
||||||
const allNotes = data.noteMap.map((note) => note)
|
const allNotes = data.noteMap.map((note) => note)
|
||||||
this.contextNotes = allNotes
|
this.contextNotes = allNotes
|
||||||
@@ -366,7 +398,7 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
// get notes in the current folder
|
// get notes in the current folder
|
||||||
getContextNotes () {
|
getContextNotes () {
|
||||||
const { data, params } = this.props
|
const { data, match: { params } } = this.props
|
||||||
const storageKey = params.storageKey
|
const storageKey = params.storageKey
|
||||||
const folderKey = params.folderKey
|
const folderKey = params.folderKey
|
||||||
const storage = data.storageMap.get(storageKey)
|
const storage = data.storageMap.get(storageKey)
|
||||||
@@ -406,8 +438,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNoteClick (e, uniqueKey) {
|
handleNoteClick (e, uniqueKey) {
|
||||||
const { router } = this.context
|
const { dispatch, location } = this.props
|
||||||
const { location } = this.props
|
|
||||||
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
||||||
const { ctrlKeyDown, shiftKeyDown } = this.state
|
const { ctrlKeyDown, shiftKeyDown } = this.state
|
||||||
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||||
@@ -458,16 +489,16 @@ class NoteList extends React.Component {
|
|||||||
prevShiftNoteIndex
|
prevShiftNoteIndex
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push({
|
dispatch(push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: uniqueKey
|
key: uniqueKey
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortByChange (e) {
|
handleSortByChange (e) {
|
||||||
const { dispatch, params: { folderKey } } = this.props
|
const { dispatch, match: { params: { folderKey } } } = this.props
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
[folderKey]: { sortBy: e.target.value }
|
[folderKey]: { sortBy: e.target.value }
|
||||||
@@ -494,14 +525,22 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
alertIfSnippet () {
|
alertIfSnippet (msg) {
|
||||||
|
const warningMessage = (msg) => ({
|
||||||
|
'export-txt': 'Text export',
|
||||||
|
'export-md': 'Markdown export',
|
||||||
|
'export-html': 'HTML export',
|
||||||
|
'export-pdf': 'PDF export',
|
||||||
|
'print': 'Print'
|
||||||
|
})[msg]
|
||||||
|
|
||||||
const targetIndex = this.getTargetIndex()
|
const targetIndex = this.getTargetIndex()
|
||||||
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Sorry!'),
|
message: i18n.__('Sorry!'),
|
||||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
||||||
buttons: [i18n.__('OK'), i18n.__('Cancel')]
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -652,14 +691,18 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
data.forEach((item) => {
|
const dispatchHandler = () => {
|
||||||
dispatch({
|
data.forEach((item) => {
|
||||||
type: 'DELETE_NOTE',
|
dispatch({
|
||||||
storageKey: item.storageKey,
|
type: 'DELETE_NOTE',
|
||||||
noteKey: item.noteKey
|
storageKey: item.storageKey,
|
||||||
|
noteKey: item.noteKey
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
ee.once('list:next', dispatchHandler)
|
||||||
})
|
})
|
||||||
|
.then(() => ee.emit('list:next'))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Cannot Delete note: ' + err)
|
console.error('Cannot Delete note: ' + err)
|
||||||
})
|
})
|
||||||
@@ -683,6 +726,7 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||||
})
|
})
|
||||||
|
.then(() => ee.emit('list:next'))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Notes could not go to trash: ' + err)
|
console.error('Notes could not go to trash: ' + err)
|
||||||
})
|
})
|
||||||
@@ -706,7 +750,12 @@ class NoteList extends React.Component {
|
|||||||
type: firstNote.type,
|
type: firstNote.type,
|
||||||
folder: folder.key,
|
folder: folder.key,
|
||||||
title: firstNote.title + ' ' + i18n.__('copy'),
|
title: firstNote.title + ' ' + i18n.__('copy'),
|
||||||
content: firstNote.content
|
content: firstNote.content,
|
||||||
|
linesHighlighted: firstNote.linesHighlighted,
|
||||||
|
description: firstNote.description,
|
||||||
|
snippets: firstNote.snippets,
|
||||||
|
tags: firstNote.tags,
|
||||||
|
isStarred: firstNote.isStarred
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
attachmentManagement.cloneAttachments(firstNote, note)
|
attachmentManagement.cloneAttachments(firstNote, note)
|
||||||
@@ -722,10 +771,10 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys: [note.key]
|
selectedNoteKeys: [note.key]
|
||||||
})
|
})
|
||||||
|
|
||||||
hashHistory.push({
|
dispatch(push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {key: note.key}
|
search: queryString.stringify({key: note.key})
|
||||||
})
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,13 +784,13 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
navigate (sender, pathname) {
|
navigate (sender, pathname) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push({
|
dispatch(push({
|
||||||
pathname,
|
pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
// key: noteKey
|
// key: noteKey
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
save (note) {
|
save (note) {
|
||||||
@@ -871,7 +920,7 @@ class NoteList extends React.Component {
|
|||||||
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
|
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add notes to the current folder
|
// Add notes to the current folder
|
||||||
addNotesFromFiles (filepaths) {
|
addNotesFromFiles (filepaths) {
|
||||||
const { dispatch, location } = this.props
|
const { dispatch, location } = this.props
|
||||||
const { storage, folder } = this.resolveTargetFolder()
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
@@ -895,13 +944,20 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
dataApi.createNote(storage.key, newNote)
|
dataApi.createNote(storage.key, newNote)
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
dispatch({
|
attachmentManagement.importAttachments(note.content, filepath, storage.key, note.key)
|
||||||
type: 'UPDATE_NOTE',
|
.then((newcontent) => {
|
||||||
note: note
|
note.content = newcontent
|
||||||
})
|
|
||||||
hashHistory.push({
|
dataApi.updateNote(storage.key, note.key, note)
|
||||||
pathname: location.pathname,
|
|
||||||
query: {key: getNoteKey(note)}
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
dispatch(push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
search: queryString.stringify({key: getNoteKey(note)})
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -911,14 +967,15 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
getTargetIndex () {
|
getTargetIndex () {
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
|
const key = queryString.parse(location.search).key
|
||||||
const targetIndex = _.findIndex(this.notes, (note) => {
|
const targetIndex = _.findIndex(this.notes, (note) => {
|
||||||
return getNoteKey(note) === location.query.key
|
return getNoteKey(note) === key
|
||||||
})
|
})
|
||||||
return targetIndex
|
return targetIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveTargetFolder () {
|
resolveTargetFolder () {
|
||||||
const { data, params } = this.props
|
const { data, match: { params } } = this.props
|
||||||
let storage = data.storageMap.get(params.storageKey)
|
let storage = data.storageMap.get(params.storageKey)
|
||||||
|
|
||||||
// Find first storage
|
// Find first storage
|
||||||
@@ -966,7 +1023,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { location, config, params: { folderKey } } = this.props
|
const { location, config, match: { params: { folderKey } } } = this.props
|
||||||
let { notes } = this.props
|
let { notes } = this.props
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
||||||
@@ -1042,6 +1099,7 @@ class NoteList extends React.Component {
|
|||||||
storageName={this.getNoteStorage(note).name}
|
storageName={this.getNoteStorage(note).name}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||||
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1088,7 +1146,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
||||||
>
|
>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
<img src='../resources/icon/icon-column.svg' />
|
||||||
</button>
|
</button>
|
||||||
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
|
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
@@ -1096,7 +1154,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
||||||
>
|
>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-column-list.svg' />
|
<img src='../resources/icon/icon-column-list.svg' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1105,6 +1163,7 @@ class NoteList extends React.Component {
|
|||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
||||||
onKeyUp={this.handleNoteListKeyUp}
|
onKeyUp={this.handleNoteListKeyUp}
|
||||||
|
onBlur={this.handleNoteListBlur}
|
||||||
>
|
>
|
||||||
{noteList}
|
{noteList}
|
||||||
</div>
|
</div>
|
||||||
@@ -1124,4 +1183,4 @@ NoteList.propTypes = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default debounceRender(CSSModules(NoteList, styles))
|
export default CSSModules(NoteList, styles)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const PreferenceButton = ({
|
|||||||
onClick
|
onClick
|
||||||
}) => (
|
}) => (
|
||||||
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
|
<img src='../resources/icon/icon-setting.svg' />
|
||||||
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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'
|
||||||
import styles from './StorageItem.styl'
|
import styles from './StorageItem.styl'
|
||||||
import { hashHistory } from 'react-router'
|
|
||||||
import modal from 'browser/main/lib/modal'
|
import modal from 'browser/main/lib/modal'
|
||||||
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
|
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
|
||||||
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
||||||
@@ -12,6 +11,7 @@ import _ from 'lodash'
|
|||||||
import { SortableElement } from 'react-sortable-hoc'
|
import { SortableElement } from 'react-sortable-hoc'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
@@ -25,7 +25,8 @@ class StorageItem extends React.Component {
|
|||||||
const { storage } = this.props
|
const { storage } = this.props
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isOpen: !!storage.isOpen
|
isOpen: !!storage.isOpen,
|
||||||
|
draggedOver: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,14 +134,14 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleHeaderInfoClick (e) {
|
handleHeaderInfoClick (e) {
|
||||||
const { storage } = this.props
|
const { storage, dispatch } = this.props
|
||||||
hashHistory.push('/storages/' + storage.key)
|
dispatch(push('/storages/' + storage.key))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFolderButtonClick (folderKey) {
|
handleFolderButtonClick (folderKey) {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
const { storage } = this.props
|
const { storage, dispatch } = this.props
|
||||||
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
|
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +205,20 @@ class StorageItem extends React.Component {
|
|||||||
folderKey: data.folderKey,
|
folderKey: data.folderKey,
|
||||||
fileType: data.fileType
|
fileType: data.fileType
|
||||||
})
|
})
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'info',
|
||||||
|
message: 'Exported to "' + data.exportDir + '"'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dialog.showErrorBox(
|
||||||
|
'Export error',
|
||||||
|
err ? err.message || err : 'Unexpected error during export'
|
||||||
|
)
|
||||||
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -231,14 +246,20 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragEnter (e) {
|
handleDragEnter (e, key) {
|
||||||
e.dataTransfer.setData('defaultColor', e.target.style.backgroundColor)
|
e.preventDefault()
|
||||||
e.target.style.backgroundColor = 'rgba(129, 130, 131, 0.08)'
|
if (this.state.draggedOver === key) { return }
|
||||||
|
this.setState({
|
||||||
|
draggedOver: key
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragLeave (e) {
|
handleDragLeave (e) {
|
||||||
e.target.style.opacity = '1'
|
e.preventDefault()
|
||||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
if (this.state.draggedOver === null) { return }
|
||||||
|
this.setState({
|
||||||
|
draggedOver: null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
dropNote (storage, folder, dispatch, location, noteData) {
|
dropNote (storage, folder, dispatch, location, noteData) {
|
||||||
@@ -263,8 +284,12 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDrop (e, storage, folder, dispatch, location) {
|
handleDrop (e, storage, folder, dispatch, location) {
|
||||||
e.target.style.opacity = '1'
|
e.preventDefault()
|
||||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
if (this.state.draggedOver !== null) {
|
||||||
|
this.setState({
|
||||||
|
draggedOver: null
|
||||||
|
})
|
||||||
|
}
|
||||||
const noteData = JSON.parse(e.dataTransfer.getData('note'))
|
const noteData = JSON.parse(e.dataTransfer.getData('note'))
|
||||||
this.dropNote(storage, folder, dispatch, location, noteData)
|
this.dropNote(storage, folder, dispatch, location, noteData)
|
||||||
}
|
}
|
||||||
@@ -274,7 +299,7 @@ class StorageItem extends React.Component {
|
|||||||
const { folderNoteMap, trashedSet } = data
|
const { folderNoteMap, trashedSet } = data
|
||||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||||
const folderList = storage.folders.map((folder, index) => {
|
const folderList = storage.folders.map((folder, index) => {
|
||||||
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||||
const isActive = !!(location.pathname.match(folderRegex))
|
const isActive = !!(location.pathname.match(folderRegex))
|
||||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||||
|
|
||||||
@@ -291,16 +316,22 @@ class StorageItem extends React.Component {
|
|||||||
<SortableStorageItemChild
|
<SortableStorageItemChild
|
||||||
key={folder.key}
|
key={folder.key}
|
||||||
index={index}
|
index={index}
|
||||||
isActive={isActive}
|
isActive={isActive || folder.key === this.state.draggedOver}
|
||||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||||
folderName={folder.name}
|
folderName={folder.name}
|
||||||
folderColor={folder.color}
|
folderColor={folder.color}
|
||||||
isFolded={isFolded}
|
isFolded={isFolded}
|
||||||
noteCount={noteCount}
|
noteCount={noteCount}
|
||||||
handleDrop={(e) => this.handleDrop(e, storage, folder, dispatch, location)}
|
handleDrop={(e) => {
|
||||||
handleDragEnter={this.handleDragEnter}
|
this.handleDrop(e, storage, folder, dispatch, location)
|
||||||
handleDragLeave={this.handleDragLeave}
|
}}
|
||||||
|
handleDragEnter={(e) => {
|
||||||
|
this.handleDragEnter(e, folder.key)
|
||||||
|
}}
|
||||||
|
handleDragLeave={(e) => {
|
||||||
|
this.handleDragLeave(e, folder)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -331,14 +362,14 @@ class StorageItem extends React.Component {
|
|||||||
<button styleName='header-addFolderButton'
|
<button styleName='header-addFolderButton'
|
||||||
onClick={(e) => this.handleAddFolderButtonClick(e)}
|
onClick={(e) => this.handleAddFolderButtonClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-plus.svg' />
|
<img src='../resources/icon/icon-plus.svg' />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<button styleName='header-info'
|
<button styleName='header-info'
|
||||||
onClick={(e) => this.handleHeaderInfoClick(e)}
|
onClick={(e) => this.handleHeaderInfoClick(e)}
|
||||||
>
|
>
|
||||||
<span styleName='header-info-name'>
|
<span>
|
||||||
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
|
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
|
||||||
</span>
|
</span>
|
||||||
{isFolded &&
|
{isFolded &&
|
||||||
@@ -349,7 +380,7 @@ class StorageItem extends React.Component {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{this.state.isOpen &&
|
{this.state.isOpen &&
|
||||||
<div styleName='folderList' >
|
<div>
|
||||||
{folderList}
|
{folderList}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import styles from './SideNav.styl'
|
import styles from './SideNav.styl'
|
||||||
@@ -20,13 +21,31 @@ import i18n from 'browser/lib/i18n'
|
|||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
import { remote } from 'electron'
|
import { remote } from 'electron'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
|
import ColorPicker from 'browser/components/ColorPicker'
|
||||||
|
import { every, sortBy } from 'lodash'
|
||||||
|
|
||||||
function matchActiveTags (tags, activeTags) {
|
function matchActiveTags (tags, activeTags) {
|
||||||
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
return every(activeTags, v => tags.indexOf(v) >= 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SideNav extends React.Component {
|
class SideNav extends React.Component {
|
||||||
// TODO: should not use electron stuff v0.7
|
// TODO: should not use electron stuff v0.7
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
colorPicker: {
|
||||||
|
show: false,
|
||||||
|
color: null,
|
||||||
|
tagName: null,
|
||||||
|
targetRect: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dismissColorPicker = this.dismissColorPicker.bind(this)
|
||||||
|
this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this)
|
||||||
|
this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
|
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
|
||||||
@@ -38,14 +57,14 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
deleteTag (tag) {
|
deleteTag (tag) {
|
||||||
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
ype: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Confirm tag deletion'),
|
message: i18n.__('Confirm tag deletion'),
|
||||||
detail: i18n.__('This will permanently remove this tag.'),
|
detail: i18n.__('This will permanently remove this tag.'),
|
||||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (selectedButton === 0) {
|
if (selectedButton === 0) {
|
||||||
const { data, dispatch, location, params } = this.props
|
const { data, dispatch, location, match: { params } } = this.props
|
||||||
|
|
||||||
const notes = data.noteMap
|
const notes = data.noteMap
|
||||||
.map(note => note)
|
.map(note => note)
|
||||||
@@ -75,7 +94,7 @@ class SideNav extends React.Component {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tags.splice(index, 1)
|
tags.splice(index, 1)
|
||||||
|
|
||||||
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
|
dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -87,13 +106,13 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleHomeButtonClick (e) {
|
handleHomeButtonClick (e) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/home')
|
dispatch(push('/home'))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStarredButtonClick (e) {
|
handleStarredButtonClick (e) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/starred')
|
dispatch(push('/starred'))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagContextMenu (e, tag) {
|
handleTagContextMenu (e, tag) {
|
||||||
@@ -104,9 +123,64 @@ class SideNav extends React.Component {
|
|||||||
click: this.deleteTag.bind(this, tag)
|
click: this.deleteTag.bind(this, tag)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
menu.push({
|
||||||
|
label: i18n.__('Customize Color'),
|
||||||
|
click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect())
|
||||||
|
})
|
||||||
|
|
||||||
context.popup(menu)
|
context.popup(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dismissColorPicker () {
|
||||||
|
this.setState({
|
||||||
|
colorPicker: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
displayColorPicker (tagName, rect) {
|
||||||
|
const { config } = this.props
|
||||||
|
this.setState({
|
||||||
|
colorPicker: {
|
||||||
|
show: true,
|
||||||
|
color: config.coloredTags[tagName],
|
||||||
|
tagName,
|
||||||
|
targetRect: rect
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleColorPickerConfirm (color) {
|
||||||
|
const { dispatch, config: {coloredTags} } = this.props
|
||||||
|
const { colorPicker: { tagName } } = this.state
|
||||||
|
const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex})
|
||||||
|
|
||||||
|
const config = { coloredTags: newColoredTags }
|
||||||
|
ConfigManager.set(config)
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config
|
||||||
|
})
|
||||||
|
this.dismissColorPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleColorPickerReset () {
|
||||||
|
const { dispatch, config: {coloredTags} } = this.props
|
||||||
|
const { colorPicker: { tagName } } = this.state
|
||||||
|
const newColoredTags = Object.assign({}, coloredTags)
|
||||||
|
|
||||||
|
delete newColoredTags[tagName]
|
||||||
|
|
||||||
|
const config = { coloredTags: newColoredTags }
|
||||||
|
ConfigManager.set(config)
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config
|
||||||
|
})
|
||||||
|
this.dismissColorPicker()
|
||||||
|
}
|
||||||
|
|
||||||
handleToggleButtonClick (e) {
|
handleToggleButtonClick (e) {
|
||||||
const { dispatch, config } = this.props
|
const { dispatch, config } = this.props
|
||||||
|
|
||||||
@@ -118,18 +192,18 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTrashedButtonClick (e) {
|
handleTrashedButtonClick (e) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/trashed')
|
dispatch(push('/trashed'))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSwitchFoldersButtonClick () {
|
handleSwitchFoldersButtonClick () {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/home')
|
dispatch(push('/home'))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSwitchTagsButtonClick () {
|
handleSwitchTagsButtonClick () {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/alltags')
|
dispatch(push('/alltags'))
|
||||||
}
|
}
|
||||||
|
|
||||||
onSortEnd (storage) {
|
onSortEnd (storage) {
|
||||||
@@ -198,6 +272,7 @@ class SideNav extends React.Component {
|
|||||||
<div styleName='tagList'>
|
<div styleName='tagList'>
|
||||||
{this.tagListComponent(data)}
|
{this.tagListComponent(data)}
|
||||||
</div>
|
</div>
|
||||||
|
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -207,9 +282,10 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
tagListComponent () {
|
tagListComponent () {
|
||||||
const { data, location, config } = this.props
|
const { data, location, config } = this.props
|
||||||
|
const { colorPicker } = this.state
|
||||||
const activeTags = this.getActiveTags(location.pathname)
|
const activeTags = this.getActiveTags(location.pathname)
|
||||||
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
||||||
let tagList = _.sortBy(data.tagNoteMap.map(
|
let tagList = sortBy(data.tagNoteMap.map(
|
||||||
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
||||||
).filter(
|
).filter(
|
||||||
tag => tag.size > 0
|
tag => tag.size > 0
|
||||||
@@ -222,7 +298,7 @@ class SideNav extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (config.sortTagsBy === 'COUNTER') {
|
if (config.sortTagsBy === 'COUNTER') {
|
||||||
tagList = _.sortBy(tagList, item => (0 - item.size))
|
tagList = sortBy(tagList, item => (0 - item.size))
|
||||||
}
|
}
|
||||||
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
|
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
|
||||||
tagList = tagList.filter(
|
tagList = tagList.filter(
|
||||||
@@ -237,10 +313,11 @@ class SideNav extends React.Component {
|
|||||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||||
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||||
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
||||||
isActive={this.getTagActive(location.pathname, tag.name)}
|
isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)}
|
||||||
isRelated={tag.related}
|
isRelated={tag.related}
|
||||||
key={tag.name}
|
key={tag.name}
|
||||||
count={tag.size}
|
count={tag.size}
|
||||||
|
color={config.coloredTags[tag.name]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -274,8 +351,8 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClickTagListItem (name) {
|
handleClickTagListItem (name) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push(`/tags/${encodeURIComponent(name)}`)
|
dispatch(push(`/tags/${encodeURIComponent(name)}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortTagsByChange (e) {
|
handleSortTagsByChange (e) {
|
||||||
@@ -293,8 +370,7 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClickNarrowToTag (tag) {
|
handleClickNarrowToTag (tag) {
|
||||||
const { router } = this.context
|
const { dispatch, location } = this.props
|
||||||
const { location } = this.props
|
|
||||||
const listOfTags = this.getActiveTags(location.pathname)
|
const listOfTags = this.getActiveTags(location.pathname)
|
||||||
const indexOfTag = listOfTags.indexOf(tag)
|
const indexOfTag = listOfTags.indexOf(tag)
|
||||||
if (indexOfTag > -1) {
|
if (indexOfTag > -1) {
|
||||||
@@ -302,7 +378,7 @@ class SideNav extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
listOfTags.push(tag)
|
listOfTags.push(tag)
|
||||||
}
|
}
|
||||||
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
|
dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
emptyTrash (entries) {
|
emptyTrash (entries) {
|
||||||
@@ -333,6 +409,7 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { data, location, config, dispatch } = this.props
|
const { data, location, config, dispatch } = this.props
|
||||||
|
const { colorPicker: colorPickerState } = this.state
|
||||||
|
|
||||||
const isFolded = config.isSideNavFolded
|
const isFolded = config.isSideNavFolded
|
||||||
|
|
||||||
@@ -349,9 +426,23 @@ class SideNav extends React.Component {
|
|||||||
useDragHandle
|
useDragHandle
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let colorPicker
|
||||||
|
if (colorPickerState.show) {
|
||||||
|
colorPicker = (
|
||||||
|
<ColorPicker
|
||||||
|
color={colorPickerState.color}
|
||||||
|
targetRect={colorPickerState.targetRect}
|
||||||
|
onConfirm={this.handleColorPickerConfirm}
|
||||||
|
onCancel={this.dismissColorPicker}
|
||||||
|
onReset={this.handleColorPickerReset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const style = {}
|
const style = {}
|
||||||
if (!isFolded) style.width = this.props.width
|
if (!isFolded) style.width = this.props.width
|
||||||
const isTagActive = location.pathname.match(/tag/)
|
const isTagActive = /tag/.test(location.pathname)
|
||||||
return (
|
return (
|
||||||
<div className='SideNav'
|
<div className='SideNav'
|
||||||
styleName={isFolded ? 'root--folded' : 'root'}
|
styleName={isFolded ? 'root--folded' : 'root'}
|
||||||
@@ -368,6 +459,7 @@ class SideNav extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.SideNavComponent(isFolded, storageList)}
|
{this.SideNavComponent(isFolded, storageList)}
|
||||||
|
{colorPicker}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,14 @@
|
|||||||
.update-icon
|
.update-icon
|
||||||
color $brand-color
|
color $brand-color
|
||||||
|
|
||||||
|
body[data-theme="default"]
|
||||||
|
.zoom
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
|
body[data-theme="white"]
|
||||||
|
.zoom
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import styles from './StatusBar.styl'
|
|||||||
import ZoomManager from 'browser/main/lib/ZoomManager'
|
import ZoomManager from 'browser/main/lib/ZoomManager'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
|
import EventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote, ipcRenderer } = electron
|
const { remote, ipcRenderer } = electron
|
||||||
@@ -13,6 +14,26 @@ const { dialog } = remote
|
|||||||
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
||||||
|
|
||||||
class StatusBar extends React.Component {
|
class StatusBar extends React.Component {
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
|
||||||
|
this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this)
|
||||||
|
this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
EventEmitter.on('status:zoomin', this.handleZoomInMenuItem)
|
||||||
|
EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem)
|
||||||
|
EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
EventEmitter.off('status:zoomin', this.handleZoomInMenuItem)
|
||||||
|
EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem)
|
||||||
|
EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem)
|
||||||
|
}
|
||||||
|
|
||||||
updateApp () {
|
updateApp () {
|
||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@@ -48,6 +69,20 @@ class StatusBar extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleZoomInMenuItem () {
|
||||||
|
const zoomFactor = ZoomManager.getZoom() + 0.1
|
||||||
|
this.handleZoomMenuItemClick(zoomFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleZoomOutMenuItem () {
|
||||||
|
const zoomFactor = ZoomManager.getZoom() - 0.1
|
||||||
|
this.handleZoomMenuItemClick(zoomFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleZoomResetMenuItem () {
|
||||||
|
this.handleZoomMenuItemClick(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { config, status } = this.context
|
const { config, status } = this.context
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import _ from 'lodash'
|
|||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import NewNoteButton from 'browser/main/NewNoteButton'
|
import NewNoteButton from 'browser/main/NewNoteButton'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import debounce from 'lodash/debounce'
|
||||||
|
import CInput from 'react-composition-input'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
class TopBar extends React.Component {
|
class TopBar extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -14,22 +17,36 @@ class TopBar extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
search: '',
|
search: '',
|
||||||
searchOptions: [],
|
searchOptions: [],
|
||||||
isSearching: false,
|
isSearching: false
|
||||||
isAlphabet: false,
|
|
||||||
isIME: false,
|
|
||||||
isConfirmTranslation: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { dispatch } = this.props
|
||||||
|
|
||||||
this.focusSearchHandler = () => {
|
this.focusSearchHandler = () => {
|
||||||
this.handleOnSearchFocus()
|
this.handleOnSearchFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.codeInitHandler = this.handleCodeInit.bind(this)
|
this.codeInitHandler = this.handleCodeInit.bind(this)
|
||||||
|
this.handleKeyDown = this.handleKeyDown.bind(this)
|
||||||
|
this.handleSearchFocus = this.handleSearchFocus.bind(this)
|
||||||
|
this.handleSearchBlur = this.handleSearchBlur.bind(this)
|
||||||
|
this.handleSearchChange = this.handleSearchChange.bind(this)
|
||||||
|
this.handleSearchClearButton = this.handleSearchClearButton.bind(this)
|
||||||
|
|
||||||
|
this.debouncedUpdateKeyword = debounce((keyword) => {
|
||||||
|
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
|
||||||
|
this.setState({
|
||||||
|
search: keyword
|
||||||
|
})
|
||||||
|
ee.emit('top:search', keyword)
|
||||||
|
}, 1000 / 60, {
|
||||||
|
maxWait: 1000 / 8
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { params } = this.props
|
const { match: { params } } = this.props
|
||||||
const searchWord = params.searchword
|
const searchWord = params && params.searchword
|
||||||
if (searchWord !== undefined) {
|
if (searchWord !== undefined) {
|
||||||
this.setState({
|
this.setState({
|
||||||
search: searchWord,
|
search: searchWord,
|
||||||
@@ -46,22 +63,22 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSearchClearButton (e) {
|
handleSearchClearButton (e) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
this.setState({
|
this.setState({
|
||||||
search: '',
|
search: '',
|
||||||
isSearching: false
|
isSearching: false
|
||||||
})
|
})
|
||||||
this.refs.search.childNodes[0].blur
|
this.refs.search.childNodes[0].blur
|
||||||
router.push('/searched')
|
dispatch(push('/searched'))
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
this.debouncedUpdateKeyword('')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
// reset states
|
// Re-apply search field on ENTER key
|
||||||
this.setState({
|
if (e.keyCode === 13) {
|
||||||
isAlphabet: false,
|
this.debouncedUpdateKeyword(e.target.value)
|
||||||
isIME: false
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// Clear search on ESC
|
// Clear search on ESC
|
||||||
if (e.keyCode === 27) {
|
if (e.keyCode === 27) {
|
||||||
@@ -79,52 +96,11 @@ class TopBar extends React.Component {
|
|||||||
ee.emit('list:prior')
|
ee.emit('list:prior')
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the key is an alphabet, del, enter or ctr
|
|
||||||
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
|
|
||||||
this.setState({
|
|
||||||
isAlphabet: true
|
|
||||||
})
|
|
||||||
// When the key is an IME input (Japanese, Chinese)
|
|
||||||
} else if (e.keyCode === 229) {
|
|
||||||
this.setState({
|
|
||||||
isIME: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyUp (e) {
|
|
||||||
const { router } = this.context
|
|
||||||
// reset states
|
|
||||||
this.setState({
|
|
||||||
isConfirmTranslation: false
|
|
||||||
})
|
|
||||||
|
|
||||||
// When the key is translation confirmation (Enter, Space)
|
|
||||||
if (this.state.isIME && (e.keyCode === 32 || e.keyCode === 13)) {
|
|
||||||
this.setState({
|
|
||||||
isConfirmTranslation: true
|
|
||||||
})
|
|
||||||
const keyword = this.refs.searchInput.value
|
|
||||||
router.push(`/searched/${encodeURIComponent(keyword)}`)
|
|
||||||
this.setState({
|
|
||||||
search: keyword
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchChange (e) {
|
handleSearchChange (e) {
|
||||||
const { router } = this.context
|
const keyword = e.target.value
|
||||||
const keyword = this.refs.searchInput.value
|
this.debouncedUpdateKeyword(keyword)
|
||||||
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
|
||||||
router.push(`/searched/${encodeURIComponent(keyword)}`)
|
|
||||||
} else {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
search: keyword
|
|
||||||
})
|
|
||||||
ee.emit('top:search', keyword)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchFocus (e) {
|
handleSearchFocus (e) {
|
||||||
@@ -132,6 +108,7 @@ class TopBar extends React.Component {
|
|||||||
isSearching: true
|
isSearching: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchBlur (e) {
|
handleSearchBlur (e) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
@@ -161,7 +138,7 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleCodeInit () {
|
handleCodeInit () {
|
||||||
ee.emit('top:search', this.refs.searchInput.value)
|
ee.emit('top:search', this.refs.searchInput.value || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -174,24 +151,23 @@ class TopBar extends React.Component {
|
|||||||
<div styleName='control'>
|
<div styleName='control'>
|
||||||
<div styleName='control-search'>
|
<div styleName='control-search'>
|
||||||
<div styleName='control-search-input'
|
<div styleName='control-search-input'
|
||||||
onFocus={(e) => this.handleSearchFocus(e)}
|
onFocus={this.handleSearchFocus}
|
||||||
onBlur={(e) => this.handleSearchBlur(e)}
|
onBlur={this.handleSearchBlur}
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
ref='search'
|
ref='search'
|
||||||
>
|
>
|
||||||
<input
|
<CInput
|
||||||
ref='searchInput'
|
ref='searchInput'
|
||||||
value={this.state.search}
|
value={this.state.search}
|
||||||
onChange={(e) => this.handleSearchChange(e)}
|
onInputChange={this.handleSearchChange}
|
||||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
onKeyDown={this.handleKeyDown}
|
||||||
onKeyUp={(e) => this.handleKeyUp(e)}
|
|
||||||
placeholder={i18n.__('Search')}
|
placeholder={i18n.__('Search')}
|
||||||
type='text'
|
type='text'
|
||||||
className='searchInput'
|
className='searchInput'
|
||||||
/>
|
/>
|
||||||
{this.state.search !== '' &&
|
{this.state.search !== '' &&
|
||||||
<button styleName='control-search-input-clear'
|
<button styleName='control-search-input-clear'
|
||||||
onClick={(e) => this.handleSearchClearButton(e)}
|
onClick={this.handleSearchClearButton}
|
||||||
>
|
>
|
||||||
<i className='fa fa-fw fa-times' />
|
<i className='fa fa-fw fa-times' />
|
||||||
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
|
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
|
||||||
@@ -206,8 +182,8 @@ class TopBar extends React.Component {
|
|||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
'config',
|
'config',
|
||||||
'params',
|
'location',
|
||||||
'location'
|
'match'
|
||||||
])}
|
])}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ modalBackColor = white
|
|||||||
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
::-webkit-scrollbar-thumb
|
::-webkit-scrollbar-thumb
|
||||||
background-color rgba(0, 0, 0, 0.3)
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
.ModalBase
|
.ModalBase
|
||||||
@@ -148,6 +149,7 @@ body[data-theme="dark"]
|
|||||||
z-index modalZIndex + 5
|
z-index modalZIndex + 5
|
||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
|
background-color $ui-solarized-dark-backgroundColor
|
||||||
::-webkit-scrollbar-thumb
|
::-webkit-scrollbar-thumb
|
||||||
background-color rgba(0, 0, 0, 0.3)
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
.ModalBase
|
.ModalBase
|
||||||
@@ -157,6 +159,7 @@ body[data-theme="solarized-dark"]
|
|||||||
color: $ui-solarized-dark-text-color
|
color: $ui-solarized-dark-text-color
|
||||||
|
|
||||||
body[data-theme="monokai"]
|
body[data-theme="monokai"]
|
||||||
|
background-color $ui-monokai-backgroundColor
|
||||||
::-webkit-scrollbar-thumb
|
::-webkit-scrollbar-thumb
|
||||||
background-color rgba(0, 0, 0, 0.3)
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
.ModalBase
|
.ModalBase
|
||||||
@@ -166,6 +169,7 @@ body[data-theme="monokai"]
|
|||||||
color: $ui-monokai-text-color
|
color: $ui-monokai-text-color
|
||||||
|
|
||||||
body[data-theme="dracula"]
|
body[data-theme="dracula"]
|
||||||
|
background-color $ui-dracula-backgroundColor
|
||||||
::-webkit-scrollbar-thumb
|
::-webkit-scrollbar-thumb
|
||||||
background-color rgba(0, 0, 0, 0.3)
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
.ModalBase
|
.ModalBase
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import Main from './Main'
|
import Main from './Main'
|
||||||
import store from './store'
|
import { store, history } from './store'
|
||||||
import React from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
require('!!style!css!stylus?sourceMap!./global.styl')
|
require('!!style!css!stylus?sourceMap!./global.styl')
|
||||||
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
|
import { Route, Switch, Redirect } from 'react-router-dom'
|
||||||
import { syncHistoryWithStore } from 'react-router-redux'
|
import { ConnectedRouter } from 'connected-react-router'
|
||||||
|
import DevTools from './DevTools'
|
||||||
|
|
||||||
require('./lib/ipcClient')
|
require('./lib/ipcClient')
|
||||||
require('../lib/customMeta')
|
require('../lib/customMeta')
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
@@ -77,7 +79,6 @@ document.addEventListener('click', function (e) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const el = document.getElementById('content')
|
const el = document.getElementById('content')
|
||||||
const history = syncHistoryWithStore(hashHistory, store)
|
|
||||||
|
|
||||||
function notify (...args) {
|
function notify (...args) {
|
||||||
return new window.Notification(...args)
|
return new window.Notification(...args)
|
||||||
@@ -98,29 +99,24 @@ function updateApp () {
|
|||||||
|
|
||||||
ReactDOM.render((
|
ReactDOM.render((
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<Route path='/' component={Main}>
|
<Fragment>
|
||||||
<IndexRedirect to='/home' />
|
<Switch>
|
||||||
<Route path='home' />
|
<Redirect path='/' to='/home' exact />
|
||||||
<Route path='starred' />
|
<Route path='/(home|alltags|starred|trashed)' component={Main} />
|
||||||
<Route path='searched'>
|
<Route path='/searched' component={Main} exact />
|
||||||
<Route path=':searchword' />
|
<Route path='/searched/:searchword' component={Main} />
|
||||||
</Route>
|
<Redirect path='/tags' to='/alltags' exact />
|
||||||
<Route path='trashed' />
|
<Route path='/tags/:tagname' component={Main} />
|
||||||
<Route path='alltags' />
|
|
||||||
<Route path='tags'>
|
{/* storages */}
|
||||||
<IndexRedirect to='/alltags' />
|
<Redirect path='/storages' to='/home' exact />
|
||||||
<Route path=':tagname' />
|
<Route path='/storages/:storageKey' component={Main} exact />
|
||||||
</Route>
|
<Route path='/storages/:storageKey/folders/:folderKey' component={Main} />
|
||||||
<Route path='storages'>
|
</Switch>
|
||||||
<IndexRedirect to='/home' />
|
<DevTools />
|
||||||
<Route path=':storageKey'>
|
</Fragment>
|
||||||
<IndexRoute />
|
</ConnectedRouter>
|
||||||
<Route path='folders/:folderKey' />
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
</Router>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
), el, function () {
|
), el, function () {
|
||||||
const loadingCover = document.getElementById('loadingCover')
|
const loadingCover = document.getElementById('loadingCover')
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ const win = global.process.platform === 'win32'
|
|||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { ipcRenderer } = electron
|
const { ipcRenderer } = electron
|
||||||
const consts = require('browser/lib/consts')
|
const consts = require('browser/lib/consts')
|
||||||
|
const electronConfig = new (require('electron-config'))()
|
||||||
|
|
||||||
let isInitialized = false
|
let isInitialized = false
|
||||||
|
|
||||||
|
const DEFAULT_MARKDOWN_LINT_CONFIG = `{
|
||||||
|
"default": true
|
||||||
|
}`
|
||||||
|
|
||||||
export const DEFAULT_CONFIG = {
|
export const DEFAULT_CONFIG = {
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
isSideNavFolded: false,
|
isSideNavFolded: false,
|
||||||
@@ -22,10 +27,17 @@ export const DEFAULT_CONFIG = {
|
|||||||
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
|
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
|
||||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||||
amaEnabled: true,
|
amaEnabled: true,
|
||||||
|
autoUpdateEnabled: true,
|
||||||
hotkey: {
|
hotkey: {
|
||||||
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
||||||
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
||||||
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
|
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
|
||||||
|
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
|
||||||
|
prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F',
|
||||||
|
sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S',
|
||||||
|
insertDate: OSX ? 'Command + /' : 'Ctrl + /',
|
||||||
|
insertDateTime: OSX ? 'Command + Alt + /' : 'Ctrl + Shift + /',
|
||||||
|
toggleMenuBar: 'Alt'
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
language: 'en',
|
language: 'en',
|
||||||
@@ -37,18 +49,23 @@ export const DEFAULT_CONFIG = {
|
|||||||
scheduleEnd: 360,
|
scheduleEnd: 360,
|
||||||
showCopyNotification: true,
|
showCopyNotification: true,
|
||||||
disableDirectWrite: false,
|
disableDirectWrite: false,
|
||||||
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||||
|
showMenuBar: false
|
||||||
},
|
},
|
||||||
editor: {
|
editor: {
|
||||||
theme: 'base16-light',
|
theme: 'base16-light',
|
||||||
keyMap: 'sublime',
|
keyMap: 'sublime',
|
||||||
fontSize: '14',
|
fontSize: '14',
|
||||||
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
|
fontFamily: win ? 'Consolas' : 'Monaco',
|
||||||
indentType: 'space',
|
indentType: 'space',
|
||||||
indentSize: '2',
|
indentSize: '2',
|
||||||
|
lineWrapping: true,
|
||||||
enableRulers: false,
|
enableRulers: false,
|
||||||
rulers: [80, 120],
|
rulers: [80, 120],
|
||||||
displayLineNumbers: true,
|
displayLineNumbers: true,
|
||||||
|
matchingPairs: '()[]{}\'\'""$$**``~~__',
|
||||||
|
matchingTriples: '```"""\'\'\'',
|
||||||
|
explodingPairs: '[]{}``$$',
|
||||||
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
||||||
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
|
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
|
||||||
scrollPastEnd: false,
|
scrollPastEnd: false,
|
||||||
@@ -56,7 +73,18 @@ export const DEFAULT_CONFIG = {
|
|||||||
fetchUrlTitle: true,
|
fetchUrlTitle: true,
|
||||||
enableTableEditor: false,
|
enableTableEditor: false,
|
||||||
enableFrontMatterTitle: true,
|
enableFrontMatterTitle: true,
|
||||||
frontMatterTitleField: 'title'
|
frontMatterTitleField: 'title',
|
||||||
|
spellcheck: false,
|
||||||
|
enableSmartPaste: false,
|
||||||
|
enableMarkdownLint: false,
|
||||||
|
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
|
||||||
|
prettierConfig: ` {
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}`,
|
||||||
|
deleteUnusedAttachments: true
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
fontSize: '14',
|
fontSize: '14',
|
||||||
@@ -74,8 +102,10 @@ export const DEFAULT_CONFIG = {
|
|||||||
breaks: true,
|
breaks: true,
|
||||||
smartArrows: false,
|
smartArrows: false,
|
||||||
allowCustomCSS: false,
|
allowCustomCSS: false,
|
||||||
customCSS: '',
|
|
||||||
|
customCSS: '/* Drop Your Custom CSS Code Here */',
|
||||||
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||||
|
mermaidHTMLLabel: false,
|
||||||
lineThroughCheckbox: true
|
lineThroughCheckbox: true
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
@@ -85,7 +115,8 @@ export const DEFAULT_CONFIG = {
|
|||||||
token: '',
|
token: '',
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: ''
|
||||||
}
|
},
|
||||||
|
coloredTags: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate (config) {
|
function validate (config) {
|
||||||
@@ -98,7 +129,6 @@ function validate (config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _save (config) {
|
function _save (config) {
|
||||||
console.log(config)
|
|
||||||
window.localStorage.setItem('config', JSON.stringify(config))
|
window.localStorage.setItem('config', JSON.stringify(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +148,8 @@ function get () {
|
|||||||
_save(config)
|
_save(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.autoUpdateEnabled = electronConfig.get('autoUpdateEnabled', config.autoUpdateEnabled)
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
let editorTheme = document.getElementById('editorTheme')
|
let editorTheme = document.getElementById('editorTheme')
|
||||||
@@ -128,16 +160,12 @@ function get () {
|
|||||||
document.head.appendChild(editorTheme)
|
document.head.appendChild(editorTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
const theme = consts.THEMES.find(theme => theme.name === config.editor.theme)
|
||||||
? config.editor.theme
|
|
||||||
: 'default'
|
|
||||||
|
|
||||||
if (config.editor.theme !== 'default') {
|
if (theme) {
|
||||||
if (config.editor.theme.startsWith('solarized')) {
|
editorTheme.setAttribute('href', theme.path)
|
||||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
|
} else {
|
||||||
} else {
|
config.editor.theme = 'default'
|
||||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +174,13 @@ function get () {
|
|||||||
|
|
||||||
function set (updates) {
|
function set (updates) {
|
||||||
const currentConfig = get()
|
const currentConfig = get()
|
||||||
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
|
|
||||||
|
const arrangedUpdates = updates
|
||||||
|
if (updates.preview !== undefined && updates.preview.customCSS === '') {
|
||||||
|
arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, arrangedUpdates)
|
||||||
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
|
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
|
||||||
_save(newConfig)
|
_save(newConfig)
|
||||||
|
|
||||||
@@ -162,18 +196,15 @@ function set (updates) {
|
|||||||
editorTheme.setAttribute('rel', 'stylesheet')
|
editorTheme.setAttribute('rel', 'stylesheet')
|
||||||
document.head.appendChild(editorTheme)
|
document.head.appendChild(editorTheme)
|
||||||
}
|
}
|
||||||
const newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
|
|
||||||
? newConfig.editor.theme
|
|
||||||
: 'default'
|
|
||||||
|
|
||||||
if (newTheme !== 'default') {
|
const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme)
|
||||||
if (newTheme.startsWith('solarized')) {
|
|
||||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
|
if (newTheme) {
|
||||||
} else {
|
editorTheme.setAttribute('href', newTheme.path)
|
||||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
electronConfig.set('autoUpdateEnabled', newConfig.autoUpdateEnabled)
|
||||||
|
|
||||||
ipcRenderer.send('config-renew', {
|
ipcRenderer.send('config-renew', {
|
||||||
config: get()
|
config: get()
|
||||||
})
|
})
|
||||||
@@ -196,7 +227,7 @@ function assignConfigValues (originalConfig, rcConfig) {
|
|||||||
function rewriteHotkey (config) {
|
function rewriteHotkey (config) {
|
||||||
const keys = [...Object.keys(config.hotkey)]
|
const keys = [...Object.keys(config.hotkey)]
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
|
config.hotkey[key] = config.hotkey[key].replace(/Cmd\s/g, 'Command ')
|
||||||
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
|
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
|
||||||
})
|
})
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ const mdurl = require('mdurl')
|
|||||||
const fse = require('fs-extra')
|
const fse = require('fs-extra')
|
||||||
const escapeStringRegexp = require('escape-string-regexp')
|
const escapeStringRegexp = require('escape-string-regexp')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
|
const url = require('url')
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import { isString } from 'lodash'
|
||||||
|
|
||||||
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
||||||
const DESTINATION_FOLDER = 'attachments'
|
const DESTINATION_FOLDER = 'attachments'
|
||||||
@@ -18,15 +20,23 @@ const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(
|
|||||||
* @returns {Promise<Image>} Image element created
|
* @returns {Promise<Image>} Image element created
|
||||||
*/
|
*/
|
||||||
function getImage (file) {
|
function getImage (file) {
|
||||||
return new Promise((resolve) => {
|
if (isString(file)) {
|
||||||
const reader = new FileReader()
|
return new Promise(resolve => {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.onload = () => resolve(img)
|
img.onload = () => resolve(img)
|
||||||
reader.onload = e => {
|
img.src = file
|
||||||
img.src = e.target.result
|
})
|
||||||
}
|
} else {
|
||||||
reader.readAsDataURL(file)
|
return new Promise(resolve => {
|
||||||
})
|
const reader = new FileReader()
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => resolve(img)
|
||||||
|
reader.onload = e => {
|
||||||
|
img.src = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +86,7 @@ function getOrientation (file) {
|
|||||||
return view.getUint16(offset + (i * 12) + 8, little)
|
return view.getUint16(offset + (i * 12) + 8, little)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
|
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker.
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
offset += view.getUint16(offset, false)
|
offset += view.getUint16(offset, false)
|
||||||
@@ -151,23 +161,28 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
|
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
|
||||||
if (!fs.existsSync(sourceFilePath) && !isBase64) {
|
if (!isBase64 && !fs.existsSync(sourceFilePath)) {
|
||||||
return reject('source file does not exist')
|
return reject('source file does not exist')
|
||||||
}
|
}
|
||||||
const targetStorage = findStorage.findStorage(storageKey)
|
|
||||||
|
const sourcePath = sourceFilePath.sourceFilePath || sourceFilePath
|
||||||
|
const sourceURL = url.parse(/^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath)
|
||||||
|
|
||||||
let destinationName
|
let destinationName
|
||||||
if (useRandomName) {
|
if (useRandomName) {
|
||||||
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath.sourceFilePath || sourceFilePath)}`
|
destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) || '.png'}`
|
||||||
} else {
|
} else {
|
||||||
destinationName = path.basename(sourceFilePath.sourceFilePath || sourceFilePath)
|
destinationName = path.basename(sourceURL.pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
|
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
|
||||||
|
|
||||||
if (isBase64) {
|
if (isBase64) {
|
||||||
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
|
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
|
||||||
const dataBuffer = new Buffer(base64Data, 'base64')
|
const dataBuffer = Buffer.from(base64Data, 'base64')
|
||||||
outputFile.write(dataBuffer, () => {
|
outputFile.write(dataBuffer, () => {
|
||||||
resolve(destinationName)
|
resolve(destinationName)
|
||||||
})
|
})
|
||||||
@@ -227,9 +242,20 @@ function migrateAttachments (markdownContent, storagePath, noteKey) {
|
|||||||
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
|
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
|
||||||
*/
|
*/
|
||||||
function fixLocalURLS (renderedHTML, storagePath) {
|
function fixLocalURLS (renderedHTML, storagePath) {
|
||||||
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) {
|
const encodedWin32SeparatorRegex = /%5C/g
|
||||||
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
|
const storageRegex = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g')
|
||||||
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
|
const storageUrl = 'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/')
|
||||||
|
|
||||||
|
/*
|
||||||
|
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
|
||||||
|
|
||||||
|
- `STORAGE_FOLDER_PLACEHOLDER` will match `:storage`
|
||||||
|
- `(?:(?:\\\/|%5C)[-.\\w]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`
|
||||||
|
- `(?:\\\/|%5C)[-.\\w]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg`
|
||||||
|
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
|
||||||
|
*/
|
||||||
|
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
|
||||||
|
return match.replace(encodedWin32SeparatorRegex, '/').replace(storageRegex, storageUrl)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,22 +279,87 @@ function generateAttachmentMarkdown (fileName, path, showPreview) {
|
|||||||
* @param {Event} dropEvent DropEvent
|
* @param {Event} dropEvent DropEvent
|
||||||
*/
|
*/
|
||||||
function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||||
const file = dropEvent.dataTransfer.files[0]
|
|
||||||
const filePath = file.path
|
|
||||||
const originalFileName = path.basename(filePath)
|
|
||||||
const fileType = file['type']
|
|
||||||
const isImage = fileType.startsWith('image')
|
|
||||||
let promise
|
let promise
|
||||||
if (isImage) {
|
if (dropEvent.dataTransfer.files.length > 0) {
|
||||||
promise = fixRotate(file).then(base64data => {
|
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
|
||||||
return copyAttachment({type: 'base64', data: base64data, sourceFilePath: filePath}, storageKey, noteKey)
|
const filePath = file.path
|
||||||
})
|
const fileType = file.type // EX) 'image/gif' or 'text/html'
|
||||||
|
if (fileType.startsWith('image')) {
|
||||||
|
if (fileType === 'image/gif' || fileType === 'image/svg+xml') {
|
||||||
|
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
|
||||||
|
fileName,
|
||||||
|
title: path.basename(filePath),
|
||||||
|
isImage: true
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
return getOrientation(file)
|
||||||
|
.then((orientation) => {
|
||||||
|
if (orientation === -1) { // The image rotation is correct and does not need adjustment
|
||||||
|
return copyAttachment(filePath, storageKey, noteKey)
|
||||||
|
} else {
|
||||||
|
return fixRotate(file).then(data => copyAttachment({
|
||||||
|
type: 'base64',
|
||||||
|
data: data,
|
||||||
|
sourceFilePath: filePath
|
||||||
|
}, storageKey, noteKey))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(fileName =>
|
||||||
|
({
|
||||||
|
fileName,
|
||||||
|
title: path.basename(filePath),
|
||||||
|
isImage: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
|
||||||
|
fileName,
|
||||||
|
title: path.basename(filePath),
|
||||||
|
isImage: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}))
|
||||||
} else {
|
} else {
|
||||||
promise = copyAttachment(filePath, storageKey, noteKey)
|
let imageURL = dropEvent.dataTransfer.getData('text/plain')
|
||||||
|
|
||||||
|
if (!imageURL) {
|
||||||
|
const match = /<img[^>]*[\s"']src="([^"]+)"/.exec(dropEvent.dataTransfer.getData('text/html'))
|
||||||
|
if (match) {
|
||||||
|
imageURL = match[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
promise = Promise.all([getImage(imageURL)
|
||||||
|
.then(image => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
canvas.width = image.width
|
||||||
|
canvas.height = image.height
|
||||||
|
context.drawImage(image, 0, 0)
|
||||||
|
|
||||||
|
return copyAttachment({
|
||||||
|
type: 'base64',
|
||||||
|
data: canvas.toDataURL(),
|
||||||
|
sourceFilePath: imageURL
|
||||||
|
}, storageKey, noteKey)
|
||||||
|
})
|
||||||
|
.then(fileName => ({
|
||||||
|
fileName,
|
||||||
|
title: imageURL,
|
||||||
|
isImage: true
|
||||||
|
}))
|
||||||
|
])
|
||||||
}
|
}
|
||||||
promise.then((fileName) => {
|
|
||||||
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), isImage)
|
promise.then(files => {
|
||||||
codeEditor.insertAttachmentMd(imageMd)
|
const attachments = files.filter(file => !!file).map(file => generateAttachmentMarkdown(file.title, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, file.fileName), file.isImage))
|
||||||
|
|
||||||
|
codeEditor.insertAttachmentMd(attachments.join('\n'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +370,7 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
|||||||
* @param {String} noteKey Key of the current note
|
* @param {String} noteKey Key of the current note
|
||||||
* @param {DataTransferItem} dataTransferItem Part of the past-event
|
* @param {DataTransferItem} dataTransferItem Part of the past-event
|
||||||
*/
|
*/
|
||||||
function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
|
function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
|
||||||
if (!codeEditor) {
|
if (!codeEditor) {
|
||||||
throw new Error('codeEditor has to be given')
|
throw new Error('codeEditor has to be given')
|
||||||
}
|
}
|
||||||
@@ -316,6 +407,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
|
|||||||
reader.readAsDataURL(blob)
|
reader.readAsDataURL(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
|
||||||
|
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
|
||||||
|
* @param {String} storageKey Key of the current storage
|
||||||
|
* @param {String} noteKey Key of the current note
|
||||||
|
* @param {NativeImage} image The native image
|
||||||
|
*/
|
||||||
|
function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
|
||||||
|
if (!codeEditor) {
|
||||||
|
throw new Error('codeEditor has to be given')
|
||||||
|
}
|
||||||
|
if (!storageKey) {
|
||||||
|
throw new Error('storageKey has to be given')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noteKey) {
|
||||||
|
throw new Error('noteKey has to be given')
|
||||||
|
}
|
||||||
|
if (!image) {
|
||||||
|
throw new Error('image has to be given')
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
|
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
|
|
||||||
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
|
|
||||||
|
const imageName = `${uniqueSlug()}.png`
|
||||||
|
const imagePath = path.join(destinationDir, imageName)
|
||||||
|
|
||||||
|
const binaryData = image.toPNG()
|
||||||
|
fs.writeFileSync(imagePath, binaryData, 'binary')
|
||||||
|
|
||||||
|
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
|
||||||
|
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
|
||||||
|
codeEditor.insertAttachmentMd(imageMd)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Returns all attachment paths of the given markdown
|
* @description Returns all attachment paths of the given markdown
|
||||||
* @param {String} markdownContent content in which the attachment paths should be found
|
* @param {String} markdownContent content in which the attachment paths should be found
|
||||||
@@ -342,6 +471,54 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Copies the attachments to the storage folder and returns the mardown content it should be replaced with
|
||||||
|
* @param {String} markDownContent content in which the attachment paths should be found
|
||||||
|
* @param {String} filepath The path of the file with attachments to import
|
||||||
|
* @param {String} storageKey Storage key of the destination storage
|
||||||
|
* @param {String} noteKey Key of the current note. Will be used as subfolder in :storage
|
||||||
|
*/
|
||||||
|
function importAttachments (markDownContent, filepath, storageKey, noteKey) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const nameRegex = /(!\[.*?]\()(.+?\..+?)(\))/g
|
||||||
|
let attachPath = nameRegex.exec(markDownContent)
|
||||||
|
const promiseArray = []
|
||||||
|
const attachmentPaths = []
|
||||||
|
const groupIndex = 2
|
||||||
|
|
||||||
|
while (attachPath) {
|
||||||
|
let attachmentPath = attachPath[groupIndex]
|
||||||
|
attachmentPaths.push(attachmentPath)
|
||||||
|
attachmentPath = path.isAbsolute(attachmentPath) ? attachmentPath : path.join(path.dirname(filepath), attachmentPath)
|
||||||
|
promiseArray.push(this.copyAttachment(attachmentPath, storageKey, noteKey))
|
||||||
|
attachPath = nameRegex.exec(markDownContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
let numResolvedPromises = 0
|
||||||
|
|
||||||
|
if (promiseArray.length === 0) {
|
||||||
|
resolve(markDownContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < promiseArray.length; j++) {
|
||||||
|
promiseArray[j]
|
||||||
|
.then((fileName) => {
|
||||||
|
const newPath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName)
|
||||||
|
markDownContent = markDownContent.replace(attachmentPaths[j], newPath)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('File does not exist in path: ' + attachmentPaths[j])
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
numResolvedPromises++
|
||||||
|
if (numResolvedPromises === promiseArray.length) {
|
||||||
|
resolve(markDownContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Moves the attachments of the current note to the new location.
|
* @description Moves the attachments of the current note to the new location.
|
||||||
* Returns a modified version of the given content so that the links to the attachments point to the new note key.
|
* Returns a modified version of the given content so that the links to the attachments point to the new note key.
|
||||||
@@ -383,7 +560,14 @@ function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
|
|||||||
* @returns {String} Input without the references
|
* @returns {String} Input without the references
|
||||||
*/
|
*/
|
||||||
function removeStorageAndNoteReferences (input, noteKey) {
|
function removeStorageAndNoteReferences (input, noteKey) {
|
||||||
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
|
return input.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'), function (match) {
|
||||||
|
const temp = match
|
||||||
|
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep)
|
||||||
|
.replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep)
|
||||||
|
.replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep)
|
||||||
|
.replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep)
|
||||||
|
return temp.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -437,11 +621,79 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
console.info('Attachment folder ("' + attachmentFolder + '") did not exist..')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Get all existing attachments related to a specific note
|
||||||
|
including their status (in use or not) and their path. Return null if there're no attachment related to note or specified parametters are invalid
|
||||||
|
* @param markdownContent markdownContent of the current note
|
||||||
|
* @param storageKey StorageKey of the current note
|
||||||
|
* @param noteKey NoteKey of the currentNote
|
||||||
|
* @return {Promise<Array<{path: String, isInUse: bool}>>} Promise returning the
|
||||||
|
list of attachments with their properties */
|
||||||
|
function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) {
|
||||||
|
if (storageKey == null || noteKey == null || markdownContent == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
|
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
|
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
|
||||||
|
const attachmentsInNoteOnlyFileNames = []
|
||||||
|
if (attachmentsInNote) {
|
||||||
|
for (let i = 0; i < attachmentsInNote.length; i++) {
|
||||||
|
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fs.existsSync(attachmentFolder)) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.readdir(attachmentFolder, (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error reading directory "' + attachmentFolder + '". Error:')
|
||||||
|
console.error(err)
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const attachments = []
|
||||||
|
for (const file of files) {
|
||||||
|
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
|
||||||
|
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
|
||||||
|
attachments.push({ path: absolutePathOfFile, isInUse: false })
|
||||||
|
} else {
|
||||||
|
attachments.push({ path: absolutePathOfFile, isInUse: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(attachments)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Remove all specified attachment paths
|
||||||
|
* @param attachments attachment paths
|
||||||
|
* @return {Promise} Promise after all attachments are removed */
|
||||||
|
function removeAttachmentsByPaths (attachments) {
|
||||||
|
const promises = []
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
fs.unlink(attachment, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Could not delete "%s"', attachment)
|
||||||
|
console.error(err)
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
promises.push(promise)
|
||||||
|
}
|
||||||
|
return Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones the attachments of a given note.
|
* Clones the attachments of a given note.
|
||||||
* Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination.
|
* Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination.
|
||||||
@@ -538,12 +790,16 @@ module.exports = {
|
|||||||
fixLocalURLS,
|
fixLocalURLS,
|
||||||
generateAttachmentMarkdown,
|
generateAttachmentMarkdown,
|
||||||
handleAttachmentDrop,
|
handleAttachmentDrop,
|
||||||
handlePastImageEvent,
|
handlePasteImageEvent,
|
||||||
|
handlePasteNativeImage,
|
||||||
getAttachmentsInMarkdownContent,
|
getAttachmentsInMarkdownContent,
|
||||||
getAbsolutePathsOfAttachmentsInContent,
|
getAbsolutePathsOfAttachmentsInContent,
|
||||||
|
importAttachments,
|
||||||
removeStorageAndNoteReferences,
|
removeStorageAndNoteReferences,
|
||||||
|
removeAttachmentsByPaths,
|
||||||
deleteAttachmentFolder,
|
deleteAttachmentFolder,
|
||||||
deleteAttachmentsNotPresentInNote,
|
deleteAttachmentsNotPresentInNote,
|
||||||
|
getAttachmentsPathAndStatus,
|
||||||
moveAttachments,
|
moveAttachments,
|
||||||
cloneAttachments,
|
cloneAttachments,
|
||||||
isAttachmentLink,
|
isAttachmentLink,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) {
|
|||||||
const dstFolder = path.dirname(dstPath)
|
const dstFolder = path.dirname(dstPath)
|
||||||
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
||||||
|
|
||||||
const input = fs.createReadStream(srcPath)
|
const input = fs.createReadStream(decodeURI(srcPath))
|
||||||
const output = fs.createWriteStream(dstPath)
|
const output = fs.createWriteStream(dstPath)
|
||||||
|
|
||||||
output.on('error', reject)
|
output.on('error', reject)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function validateInput (input) {
|
|||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case 'MARKDOWN_NOTE':
|
case 'MARKDOWN_NOTE':
|
||||||
if (!_.isString(input.content)) input.content = ''
|
if (!_.isString(input.content)) input.content = ''
|
||||||
|
if (!_.isArray(input.linesHighlighted)) input.linesHighlighted = []
|
||||||
break
|
break
|
||||||
case 'SNIPPET_NOTE':
|
case 'SNIPPET_NOTE':
|
||||||
if (!_.isString(input.description)) input.description = ''
|
if (!_.isString(input.description)) input.description = ''
|
||||||
@@ -23,7 +24,8 @@ function validateInput (input) {
|
|||||||
input.snippets = [{
|
input.snippets = [{
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'text',
|
mode: 'text',
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|||||||
86
browser/main/lib/dataApi/createNoteFromUrl.js
Normal file
86
browser/main/lib/dataApi/createNoteFromUrl.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
const http = require('http')
|
||||||
|
const https = require('https')
|
||||||
|
const { createTurndownService } = require('../../../lib/turndown')
|
||||||
|
const createNote = require('./createNote')
|
||||||
|
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
|
||||||
|
function validateUrl (str) {
|
||||||
|
if (/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(str)) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR_MESSAGES = {
|
||||||
|
ENOTFOUND: 'URL not found. Please check the URL, or your internet connection and try again.',
|
||||||
|
VALIDATION_ERROR: 'Please check if the URL follows this format: https://www.google.com',
|
||||||
|
UNEXPECTED: 'Unexpected error! Please check console for details!'
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const td = createTurndownService()
|
||||||
|
|
||||||
|
if (!validateUrl(url)) {
|
||||||
|
reject({result: false, error: ERROR_MESSAGES.VALIDATION_ERROR})
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = url.startsWith('https') ? https : http
|
||||||
|
|
||||||
|
const req = request.request(url, (res) => {
|
||||||
|
let data = ''
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk
|
||||||
|
})
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
const markdownHTML = td.turndown(data)
|
||||||
|
|
||||||
|
if (dispatch !== null) {
|
||||||
|
createNote(storage, {
|
||||||
|
type: 'MARKDOWN_NOTE',
|
||||||
|
folder: folder,
|
||||||
|
title: '',
|
||||||
|
content: markdownHTML
|
||||||
|
})
|
||||||
|
.then((note) => {
|
||||||
|
const noteHash = note.key
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
dispatch(push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {key: noteHash}
|
||||||
|
}))
|
||||||
|
ee.emit('list:jump', noteHash)
|
||||||
|
ee.emit('detail:focus')
|
||||||
|
resolve({result: true, error: null})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
createNote(storage, {
|
||||||
|
type: 'MARKDOWN_NOTE',
|
||||||
|
folder: folder,
|
||||||
|
title: '',
|
||||||
|
content: markdownHTML
|
||||||
|
}).then((note) => {
|
||||||
|
resolve({result: true, note, error: null})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
req.on('error', (e) => {
|
||||||
|
console.error('error in parsing URL', e)
|
||||||
|
reject({result: false, error: ERROR_MESSAGES[e.code] || ERROR_MESSAGES.UNEXPECTED})
|
||||||
|
})
|
||||||
|
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createNoteFromUrl
|
||||||
@@ -9,7 +9,8 @@ function createSnippet (snippetFile) {
|
|||||||
id: crypto.randomBytes(16).toString('hex'),
|
id: crypto.randomBytes(16).toString('hex'),
|
||||||
name: 'Unnamed snippet',
|
name: 'Unnamed snippet',
|
||||||
prefix: [],
|
prefix: [],
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
fetchSnippet(null, snippetFile).then((snippets) => {
|
fetchSnippet(null, snippetFile).then((snippets) => {
|
||||||
snippets.push(newSnippet)
|
snippets.push(newSnippet)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const path = require('path')
|
|||||||
const resolveStorageData = require('./resolveStorageData')
|
const resolveStorageData = require('./resolveStorageData')
|
||||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
const sander = require('sander')
|
|
||||||
const { findStorage } = require('browser/lib/findStorage')
|
const { findStorage } = require('browser/lib/findStorage')
|
||||||
const deleteSingleNote = require('./deleteNote')
|
const deleteSingleNote = require('./deleteNote')
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
import resolveStorageData from './resolveStorageData'
|
import resolveStorageData from './resolveStorageData'
|
||||||
import resolveStorageNotes from './resolveStorageNotes'
|
import resolveStorageNotes from './resolveStorageNotes'
|
||||||
|
import exportNote from './exportNote'
|
||||||
import filenamify from 'filenamify'
|
import filenamify from 'filenamify'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} storageKey
|
* @param {String} storageKey
|
||||||
@@ -43,19 +43,18 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
|||||||
.then(function exportNotes (data) {
|
.then(function exportNotes (data) {
|
||||||
const { storage, notes } = data
|
const { storage, notes } = data
|
||||||
|
|
||||||
notes
|
return Promise.all(notes
|
||||||
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
||||||
.forEach(snippet => {
|
.map(note => {
|
||||||
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
|
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
|
||||||
fs.writeFileSync(notePath, snippet.content)
|
return exportNote(note.key, storage.path, note.content, notePath, null)
|
||||||
})
|
})
|
||||||
|
).then(() => ({
|
||||||
return {
|
|
||||||
storage,
|
storage,
|
||||||
folderKey,
|
folderKey,
|
||||||
fileType,
|
fileType,
|
||||||
exportDir
|
exportDir
|
||||||
}
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,37 +4,56 @@ import { findStorage } from 'browser/lib/findStorage'
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
|
const attachmentManagement = require('./attachmentManagement')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export note together with images
|
* Export note together with attachments
|
||||||
*
|
*
|
||||||
* If images is stored in the storage, creates 'images' subfolder in target directory
|
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory
|
||||||
* and copies images to it. Changes links to images in the content of the note
|
* and copies attachments to it. Changes links to images in the content of the note
|
||||||
*
|
*
|
||||||
|
* @param {String} nodeKey key of the node that should be exported
|
||||||
* @param {String} storageKey or storage path
|
* @param {String} storageKey or storage path
|
||||||
* @param {String} noteContent Content to export
|
* @param {String} noteContent Content to export
|
||||||
* @param {String} targetPath Path to exported file
|
* @param {String} targetPath Path to exported file
|
||||||
* @param {function} outputFormatter
|
* @param {function} outputFormatter
|
||||||
* @return {Promise.<*[]>}
|
* @return {Promise.<*[]>}
|
||||||
*/
|
*/
|
||||||
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) {
|
||||||
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
||||||
const exportTasks = []
|
const exportTasks = []
|
||||||
|
|
||||||
if (!storagePath) {
|
if (!storagePath) {
|
||||||
throw new Error('Storage path is not found')
|
throw new Error('Storage path is not found')
|
||||||
}
|
}
|
||||||
|
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||||
|
noteContent,
|
||||||
|
storagePath
|
||||||
|
)
|
||||||
|
attachmentsAbsolutePaths.forEach(attachment => {
|
||||||
|
exportTasks.push({
|
||||||
|
src: attachment,
|
||||||
|
dst: attachmentManagement.DESTINATION_FOLDER
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
let exportedData = noteContent
|
let exportedData = attachmentManagement.removeStorageAndNoteReferences(
|
||||||
|
noteContent,
|
||||||
|
nodeKey
|
||||||
|
)
|
||||||
|
|
||||||
if (outputFormatter) {
|
if (outputFormatter) {
|
||||||
exportedData = outputFormatter(exportedData, exportTasks)
|
exportedData = outputFormatter(exportedData, exportTasks, targetPath)
|
||||||
|
} else {
|
||||||
|
exportedData = Promise.resolve(exportedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
||||||
|
|
||||||
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
|
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
|
||||||
.then(() => {
|
.then(() => exportedData)
|
||||||
return saveToFile(exportedData, targetPath)
|
.then(data => {
|
||||||
|
return saveToFile(data, targetPath)
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
rollbackExport(tasks)
|
rollbackExport(tasks)
|
||||||
throw err
|
throw err
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const dataApi = {
|
|||||||
exportFolder: require('./exportFolder'),
|
exportFolder: require('./exportFolder'),
|
||||||
exportStorage: require('./exportStorage'),
|
exportStorage: require('./exportStorage'),
|
||||||
createNote: require('./createNote'),
|
createNote: require('./createNote'),
|
||||||
|
createNoteFromUrl: require('./createNoteFromUrl'),
|
||||||
updateNote: require('./updateNote'),
|
updateNote: require('./updateNote'),
|
||||||
deleteNote: require('./deleteNote'),
|
deleteNote: require('./deleteNote'),
|
||||||
moveNote: require('./moveNote'),
|
moveNote: require('./moveNote'),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const resolveStorageData = require('./resolveStorageData')
|
|||||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||||
const consts = require('browser/lib/consts')
|
const consts = require('browser/lib/consts')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
/**
|
/**
|
||||||
* @return {Object} all storages and notes
|
* @return {Object} all storages and notes
|
||||||
@@ -19,11 +20,14 @@ const CSON = require('@rokt33r/season')
|
|||||||
* 2. legacy
|
* 2. legacy
|
||||||
* 3. empty directory
|
* 3. empty directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
const fetchStorages = function () {
|
const fetchStorages = function () {
|
||||||
let rawStorages
|
let rawStorages
|
||||||
try {
|
try {
|
||||||
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
|
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
|
||||||
|
// Remove storages who's location is inaccesible.
|
||||||
|
rawStorages = rawStorages.filter(storage => fs.existsSync(storage.path))
|
||||||
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
|
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to parse cached data from localStorage', e)
|
console.warn('Failed to parse cached data from localStorage', e)
|
||||||
@@ -36,6 +40,7 @@ function init () {
|
|||||||
|
|
||||||
const fetchNotes = function (storages) {
|
const fetchNotes = function (storages) {
|
||||||
const findNotesFromEachStorage = storages
|
const findNotesFromEachStorage = storages
|
||||||
|
.filter(storage => fs.existsSync(storage.path))
|
||||||
.map((storage) => {
|
.map((storage) => {
|
||||||
return resolveStorageNotes(storage)
|
return resolveStorageNotes(storage)
|
||||||
.then((notes) => {
|
.then((notes) => {
|
||||||
@@ -51,7 +56,11 @@ function init () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (unknownCount > 0) {
|
if (unknownCount > 0) {
|
||||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
try {
|
||||||
|
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error writting boostnote.json: ' + e + ' from init.js')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return notes
|
return notes
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ function importAll (storage, data) {
|
|||||||
isStarred: false,
|
isStarred: false,
|
||||||
title: article.title,
|
title: article.title,
|
||||||
content: '# ' + article.title + '\n\n' + article.content,
|
content: '# ' + article.title + '\n\n' + article.content,
|
||||||
key: noteKey
|
key: noteKey,
|
||||||
|
linesHighlighted: article.linesHighlighted
|
||||||
}
|
}
|
||||||
notes.push(newNote)
|
notes.push(newNote)
|
||||||
} else {
|
} else {
|
||||||
@@ -87,7 +88,8 @@ function importAll (storage, data) {
|
|||||||
snippets: [{
|
snippets: [{
|
||||||
name: article.mode,
|
name: article.mode,
|
||||||
mode: article.mode,
|
mode: article.mode,
|
||||||
content: article.content
|
content: article.content,
|
||||||
|
linesHighlighted: article.linesHighlighted
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
notes.push(newNote)
|
notes.push(newNote)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const resolveStorageData = require('./resolveStorageData')
|
const resolveStorageData = require('./resolveStorageData')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
|
||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
const keygen = require('browser/lib/keygen')
|
const keygen = require('browser/lib/keygen')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user