1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 18:26:26 +00:00

Compare commits

..

5 Commits
master ... v0.9

Author SHA1 Message Date
Masahide Morio
9d98f0cb03 for v0.9 2018-02-04 05:51:39 +09:00
Masahide Morio
3503233631 Merge remote-tracking branch 'upstream/master' 2018-02-04 05:40:24 +09:00
Masahide Morio
c39393c453 for Windows 32bit 2018-02-04 05:40:15 +09:00
Masahide Morio
564cc80ef7 for Windows 32bit 2018-01-17 01:01:45 +09:00
Masahide.MORIO
77f7144fbf test 2018-01-16 10:01:20 +09:00
372 changed files with 9300 additions and 256574 deletions

View File

@@ -5,9 +5,9 @@
"presets": ["react-hmre"] "presets": ["react-hmre"]
}, },
"test": { "test": {
"presets": ["env" ,"react", "es2015"], "presets": ["react", "es2015"],
"plugins": [ "plugins": [
[ "babel-plugin-webpack-alias", { "config": "<rootDir>/webpack.config.js" } ] [ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
] ]
} }
} }

View File

@@ -22,10 +22,7 @@
"fontSize": "14", "fontSize": "14",
"lineNumber": true "lineNumber": true
}, },
"sortBy": { "sortBy": "UPDATED_AT",
"default": "UPDATED_AT"
},
"sortTagsBy": "ALPHABETICAL",
"ui": { "ui": {
"defaultNote": "ALWAYS_ASK", "defaultNote": "ALWAYS_ASK",
"disableDirectWrite": false, "disableDirectWrite": false,

View File

@@ -1,16 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Space indentation
[*]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
# The indent size used in the `package.json` file cannot be changed
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
[{*.yml,*.yaml,package.json}]
indent_style = space
indent_size = 2

View File

@@ -1,4 +1,3 @@
node_modules/ node_modules/
compiled/ compiled/
dist/ dist/
extra_scripts/

View File

@@ -1,11 +1,9 @@
{ {
"extends": ["standard", "standard-jsx", "plugin:react/recommended", "prettier"], "extends": ["standard", "standard-jsx", "plugin:react/recommended"],
"plugins": ["react", "prettier"], "plugins": ["react"],
"rules": { "rules": {
"no-useless-escape": 0, "no-useless-escape": 0,
"prefer-const": ["warn", { "prefer-const": "warn",
"destructuring": "all"
}],
"no-unused-vars": "warn", "no-unused-vars": "warn",
"no-undef": "warn", "no-undef": "warn",
"no-lone-blocks": "warn", "no-lone-blocks": "warn",
@@ -13,17 +11,11 @@
"react/no-string-refs": 0, "react/no-string-refs": 0,
"react/no-find-dom-node": "warn", "react/no-find-dom-node": "warn",
"react/no-render-return-value": "warn", "react/no-render-return-value": "warn",
"react/no-deprecated": "warn", "react/no-deprecated": "warn"
"prettier/prettier": ["error"]
}, },
"globals": { "globals": {
"FileReader": true, "FileReader": true,
"localStorage": true, "localStorage": true,
"fetch": true, "fetch": true
"Image": true,
"MutationObserver": true
},
"env": {
"jest": true
} }
} }

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
issuehunt: BoostIo/Boostnote

4
.gitignore vendored
View File

@@ -9,6 +9,4 @@ node_modules/*
/secret /secret
*.log *.log
.idea .idea
.vscode .vscode
package-lock.json
config.json

View File

@@ -1,5 +0,0 @@
{
"singleQuote": true,
"semi": false,
"jsxSingleQuote": true
}

View File

@@ -1,9 +1,9 @@
language: node_js language: node_js
node_js: node_js:
- 8 - 6
script: script:
- npm run lint && npm run test - npm run lint && npm run test
- '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
@@ -17,3 +17,4 @@ deploy:
script: if [ ${TRAVIS_NODE_VERSION} = "stable" ];then docker run -v $(pwd):$(pwd) -t snapcore/snapcraft sh -c "apt update -qq script: if [ ${TRAVIS_NODE_VERSION} = "stable" ];then docker run -v $(pwd):$(pwd) -t snapcore/snapcraft sh -c "apt update -qq
&& cd $(pwd) && snapcraft && snapcraft push *.snap --release edge"; fi && cd $(pwd) && snapcraft && snapcraft push *.snap --release edge"; fi
skip_cleanup: true skip_cleanup: true

41
.vscode/launch.json vendored
View File

@@ -1,41 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "BoostNote Main",
"protocol": "inspector",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"runtimeArgs": [
"--remote-debugging-port=9223",
"--hot",
"${workspaceFolder}/index.js"
],
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
}
},
{
"type": "chrome",
"request": "attach",
"name": "BoostNote Renderer",
"port": 9223,
"webRoot": "${workspaceFolder}",
"sourceMapPathOverrides": {
"webpack:///./~/*": "${webRoot}/node_modules/*",
"webpack:///*": "${webRoot}/*"
}
}
],
"compounds": [
{
"name": "BostNote All",
"configurations": ["BoostNote Main", "BoostNote Renderer"]
}
]
}

27
.vscode/tasks.json vendored
View File

@@ -1,27 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Build Boostnote",
"group": "build",
"type": "npm",
"script": "watch",
"isBackground": true,
"presentation": {
"reveal": "always",
},
"problemMatcher": {
"pattern":[
{
"regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$",
"file": 1,
"location": 2,
"message": 3
}
]
}
}
]
}

72
Backers.md Normal file
View File

@@ -0,0 +1,72 @@
<h1 align="center">Sponsors &amp; 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
View File

@@ -1,29 +0,0 @@
# 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

View File

@@ -1,35 +1,10 @@
# Current behavior <!--
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
-->
<!-- <!--
Let us know what is currently happening. Love Boostnote? Please consider supporting us via OpenCollective:
👉 https://opencollective.com/boostnoteio
Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug.
If your issue is regarding the new Boost Note.next, please open an issue in the new repo 👉 https://github.com/BoostIO/BoostNote.next/issues.
-->
# Expected behavior
<!--
Let us know what you think should happen.
-->
# Steps to reproduce
<!--
Please be thorough, issues we can reproduce are easier to fix.
-->
1.
2.
3.
# Environment
- Boostnote version: <!-- 0.x.x -->
- OS version and name: <!-- Windows 10 / Ubuntu 18.04 / etc -->
<!--
Love Boostnote? Please consider supporting us on IssueHunt:
👉 https://issuehunt.io/repos/53266139
--> -->

View File

@@ -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 - 2019 BoostIO Copyright (C) 2017 Maisin&Co., Inc.
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

View File

@@ -1,42 +0,0 @@
<!--
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
- :white_circle: This PR will modify the UI or affects the UX
- :white_circle: This PR will add/update/delete a keybinding

View File

@@ -1,7 +0,0 @@
module.exports = {
require: jest.genMockFunction(),
match: jest.genMockFunction(),
app: jest.genMockFunction(),
remote: jest.genMockFunction(),
dialog: jest.genMockFunction()
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
.codeEditor-typo
text-decoration underline wavy red
.spellcheck-select
border: none

View File

@@ -1,77 +0,0 @@
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)

View File

@@ -1,39 +0,0 @@
.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%)

View File

@@ -1,4 +1,3 @@
/* eslint-disable camelcase */
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'
@@ -7,11 +6,9 @@ import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview' import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
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) {
super(props) super(props)
// char codes for ctrl + w // char codes for ctrl + w
@@ -21,246 +18,170 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186] this.supportMdSelectionBold = [16, 17, 186]
this.state = { this.state = {
status: status: 'PREVIEW',
props.config.editor.switchPreview === 'RIGHTCLICK'
? props.config.editor.delfaultStatus
: 'CODE',
renderValue: props.value, renderValue: props.value,
keyPressed: new Set(), keyPressed: new Set(),
isLocked: props.isLocked isLocked: false
} }
this.lockEditorCode = this.handleLockEditor.bind(this) this.lockEditorCode = () => this.handleLockEditor()
this.focusEditor = this.focusEditor.bind(this)
this.previewRef = React.createRef()
} }
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)
} }
componentDidUpdate() { componentDidUpdate () {
this.value = this.refs.code.value this.value = this.refs.code.value
} }
UNSAFE_componentWillReceiveProps(props) { componentWillReceiveProps (props) {
if (props.value !== this.props.value) { if (props.value !== this.props.value) {
this.queueRendering(props.value) this.queueRendering(props.value)
} }
} }
componentWillUnmount() { componentWillUnmount () {
this.cancelQueue() this.cancelQueue()
eventEmitter.off('editor:lock', this.lockEditorCode) eventEmitter.off('editor:lock', this.lockEditorCode)
eventEmitter.off('editor:focus', this.focusEditor)
} }
focusEditor() { queueRendering (value) {
this.setState(
{
status: 'CODE'
},
() => {
if (this.refs.code == null) {
return
}
this.refs.code.focus()
}
)
}
queueRendering(value) {
clearTimeout(this.renderTimer) clearTimeout(this.renderTimer)
this.renderTimer = setTimeout(() => { this.renderTimer = setTimeout(() => {
this.renderPreview(value) this.renderPreview(value)
}, 500) }, 500)
} }
cancelQueue() { cancelQueue () {
clearTimeout(this.renderTimer) clearTimeout(this.renderTimer)
} }
renderPreview(value) { renderPreview (value) {
this.setState({ this.setState({
renderValue: value renderValue: value
}) })
} }
setValue(value) { handleChange (e) {
this.refs.code.setValue(value)
}
handleChange(e) {
this.value = this.refs.code.value this.value = this.refs.code.value
this.props.onChange(e) this.props.onChange(e)
} }
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'
this.setState( ? 'CODE'
{ : 'PREVIEW'
status: newStatus this.setState({
}, status: newStatus
() => { }, () => {
if (newStatus === 'CODE') { if (newStatus === 'CODE') {
this.refs.code.focus() this.refs.code.focus()
} else { } else {
this.previewRef.current.focus() this.refs.preview.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
const newConfig = Object.assign({}, config)
newConfig.editor.delfaultStatus = newStatus
ConfigManager.set(newConfig)
} }
) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
})
} }
} }
handleBlur(e) { handleBlur (e) {
if (this.state.isLocked) return if (this.state.isLocked) return
this.setState({ keyPressed: new Set() }) this.setState({ keyPressed: new Set() })
const { config } = this.props const { config } = this.props
if ( if (config.editor.switchPreview === 'BLUR') {
config.editor.switchPreview === 'BLUR' ||
(config.editor.switchPreview === 'DBL_CLICK' &&
this.state.status === 'CODE')
) {
const cursorPosition = this.refs.code.editor.getCursor() const cursorPosition = this.refs.code.editor.getCursor()
this.setState( this.setState({
{ status: 'PREVIEW'
status: 'PREVIEW' }, () => {
}, this.refs.preview.focus()
() => { this.refs.preview.scrollTo(cursorPosition.line)
this.previewRef.current.focus() })
this.previewRef.current.scrollToLine(cursorPosition.line)
}
)
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
} }
} }
handleDoubleClick(e) { handlePreviewMouseDown (e) {
if (this.state.isLocked) return
this.setState({ keyPressed: new Set() })
const { config } = this.props
if (config.editor.switchPreview === 'DBL_CLICK') {
this.setState(
{
status: 'CODE'
},
() => {
this.refs.code.focus()
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
)
}
}
handlePreviewMouseDown(e) {
this.previewMouseDownedAt = new Date() this.previewMouseDownedAt = new Date()
} }
handlePreviewMouseUp(e) { handlePreviewMouseUp (e) {
const { config } = this.props const { config } = this.props
if ( if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
config.editor.switchPreview === 'BLUR' && this.setState({
new Date() - this.previewMouseDownedAt < 200 status: 'CODE'
) { }, () => {
this.setState( this.refs.code.focus()
{ })
status: 'CODE'
},
() => {
this.refs.code.focus()
}
)
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
} }
} }
handleCheckboxClick(e) { handleCheckboxClick (e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/ const idMatch = /checkbox-([0-9]+)/
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i const checkedMatch = /\[x\]/i
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/ const uncheckedMatch = /\[ \]/
const checkReplace = /\[x]/i
const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) { if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
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)) {
newLine = targetLine.replace(checkReplace, '[ ]') lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
} }
if (targetLine.match(uncheckedMatch)) { if (targetLine.match(uncheckedMatch)) {
newLine = targetLine.replace(uncheckReplace, '[x]') lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
} }
this.refs.code.setLineContent(lineIndex, newLine) this.refs.code.setValue(lines.join('\n'))
} }
} }
focus() { focus () {
if (this.state.status === 'PREVIEW') { if (this.state.status === 'PREVIEW') {
this.setState( this.setState({
{ status: 'CODE'
status: 'CODE' }, () => {
}, this.refs.code.focus()
() => { })
this.refs.code.focus()
}
)
} else { } else {
this.refs.code.focus() this.refs.code.focus()
} }
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
} }
reload() { reload () {
this.refs.code.reload() this.refs.code.reload()
this.cancelQueue() this.cancelQueue()
this.renderPreview(this.props.value) this.renderPreview(this.props.value)
} }
handleKeyDown(e) { handleKeyDown (e) {
const { config } = this.props const { config } = this.props
if (this.state.status !== 'CODE') return false if (this.state.status !== 'CODE') return false
const keyPressed = this.state.keyPressed const keyPressed = this.state.keyPressed
keyPressed.add(e.keyCode) keyPressed.add(e.keyCode)
this.setState({ keyPressed }) this.setState({ keyPressed })
const isNoteHandlerKey = el => { const isNoteHandlerKey = (el) => { return keyPressed.has(el) }
return keyPressed.has(el)
}
// These conditions are for ctrl-e and ctrl-w // These conditions are for ctrl-e and ctrl-w
if ( if (keyPressed.size === this.escapeFromEditor.length &&
keyPressed.size === this.escapeFromEditor.length && !this.state.isLocked && this.state.status === 'CODE' &&
!this.state.isLocked && this.escapeFromEditor.every(isNoteHandlerKey)) {
this.state.status === 'CODE' &&
this.escapeFromEditor.every(isNoteHandlerKey)
) {
this.handleContextMenu() this.handleContextMenu()
if (config.editor.switchPreview === 'BLUR') document.activeElement.blur() if (config.editor.switchPreview === 'BLUR') document.activeElement.blur()
} }
if ( if (keyPressed.size === this.supportMdSelectionBold.length && this.supportMdSelectionBold.every(isNoteHandlerKey)) {
keyPressed.size === this.supportMdSelectionBold.length &&
this.supportMdSelectionBold.every(isNoteHandlerKey)
) {
this.addMdAroundWord('**') this.addMdAroundWord('**')
} }
} }
addMdAroundWord(mdElement) { addMdAroundWord (mdElement) {
if (this.refs.code.editor.getSelection()) { if (this.refs.code.editor.getSelection()) {
return this.addMdAroundSelection(mdElement) return this.addMdAroundSelection(mdElement)
} }
@@ -268,64 +189,25 @@ class MarkdownEditor extends React.Component {
const word = this.refs.code.editor.findWordAt(currentCaret) const word = this.refs.code.editor.findWordAt(currentCaret)
const cmDoc = this.refs.code.editor.getDoc() const cmDoc = this.refs.code.editor.getDoc()
cmDoc.replaceRange(mdElement, word.anchor) cmDoc.replaceRange(mdElement, word.anchor)
cmDoc.replaceRange(mdElement, { cmDoc.replaceRange(mdElement, { line: word.head.line, ch: word.head.ch + mdElement.length })
line: word.head.line,
ch: word.head.ch + mdElement.length
})
} }
addMdAroundSelection(mdElement) { addMdAroundSelection (mdElement) {
this.refs.code.editor.replaceSelection( this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`
)
} }
handleDropImage(dropEvent) { handleKeyUp (e) {
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) {
const keyPressed = this.state.keyPressed const keyPressed = this.state.keyPressed
keyPressed.delete(e.keyCode) keyPressed.delete(e.keyCode)
this.setState({ keyPressed }) this.setState({ keyPressed })
} }
handleLockEditor() { handleLockEditor () {
this.setState({ isLocked: !this.state.isLocked }) this.setState({ isLocked: !this.state.isLocked })
} }
render() { render () {
const { const { className, value, config, storageKey } = this.props
className,
value,
config,
storageKey,
noteKey,
linesHighlighted,
getNote,
RTL
} = 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
@@ -333,27 +215,26 @@ class MarkdownEditor extends React.Component {
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
const previewStyle = {} const previewStyle = {}
if (this.props.ignorePreviewPointerEvents) if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
previewStyle.pointerEvents = 'none'
const storage = findStorage(storageKey) const storage = findStorage(storageKey)
return ( return (
<div <div className={className == null
className={ ? 'MarkdownEditor'
className == null ? 'MarkdownEditor' : `MarkdownEditor ${className}` : `MarkdownEditor ${className}`
} }
onContextMenu={e => this.handleContextMenu(e)} onContextMenu={(e) => this.handleContextMenu(e)}
tabIndex='-1' tabIndex='-1'
onKeyDown={e => this.handleKeyDown(e)} onKeyDown={(e) => this.handleKeyDown(e)}
onKeyUp={e => this.handleKeyUp(e)} onKeyUp={(e) => this.handleKeyUp(e)}
> >
<CodeEditor <CodeEditor styleName={this.state.status === 'CODE'
styleName={ ? 'codeEditor'
this.state.status === 'CODE' ? 'codeEditor' : 'codeEditor--hide' : 'codeEditor--hide'
} }
ref='code' ref='code'
mode='Boost Flavored Markdown' mode='GitHub Flavored Markdown'
value={value} value={value}
theme={config.editor.theme} theme={config.editor.theme}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
@@ -361,43 +242,15 @@ class MarkdownEditor extends React.Component {
fontSize={editorFontSize} fontSize={editorFontSize}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorIndentSize} indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping
matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey} storageKey={storageKey}
noteKey={noteKey} onChange={(e) => this.handleChange(e)}
fetchUrlTitle={config.editor.fetchUrlTitle} onBlur={(e) => this.handleBlur(e)}
enableTableEditor={config.editor.enableTableEditor}
linesHighlighted={linesHighlighted}
onChange={e => this.handleChange(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}
dateFormatISO8601={config.editor.dateFormatISO8601}
prettierConfig={config.editor.prettierConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL}
/> />
<MarkdownPreview <MarkdownPreview styleName={this.state.status === 'PREVIEW'
ref={this.previewRef} ? 'preview'
styleName={ : 'preview--hide'
this.state.status === 'PREVIEW' ? 'preview' : 'preview--hide'
} }
style={previewStyle} style={previewStyle}
theme={config.ui.theme} theme={config.ui.theme}
@@ -409,28 +262,15 @@ class MarkdownEditor extends React.Component {
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize} indentSize={editorIndentSize}
scrollPastEnd={config.preview.scrollPastEnd} scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes} ref='preview'
smartArrows={config.preview.smartArrows} onContextMenu={(e) => this.handleContextMenu(e)}
breaks={config.preview.breaks}
sanitize={config.preview.sanitize}
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
onContextMenu={e => this.handleContextMenu(e)}
onDoubleClick={e => this.handleDoubleClick(e)}
tabIndex='0' tabIndex='0'
value={this.state.renderValue} value={this.state.renderValue}
onMouseUp={e => this.handlePreviewMouseUp(e)} onMouseUp={(e) => this.handlePreviewMouseUp(e)}
onMouseDown={e => this.handlePreviewMouseDown(e)} onMouseDown={(e) => this.handlePreviewMouseDown(e)}
onCheckboxClick={e => this.handleCheckboxClick(e)} onCheckboxClick={(e) => this.handleCheckboxClick(e)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
getNote={getNote}
export={config.export}
onDrop={e => this.handleDropImage(e)}
RTL={RTL}
/> />
</div> </div>
) )

View File

@@ -16,6 +16,7 @@
.preview .preview
display block display block
absolute top bottom left right absolute top bottom left right
z-index 100
background-color white background-color white
height 100% height 100%
width 100% width 100%

1037
browser/components/MarkdownPreview.js Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,468 +2,76 @@ import React from 'react'
import CodeEditor from 'browser/components/CodeEditor' import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview' import MarkdownPreview from 'browser/components/MarkdownPreview'
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
import _ from 'lodash'
import styles from './MarkdownSplitEditor.styl' import styles from './MarkdownSplitEditor.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
class MarkdownSplitEditor extends React.Component { class MarkdownSplitEditor extends React.Component {
constructor(props) { constructor (props) {
super(props) super(props)
this.value = props.value this.value = props.value
this.focus = () => this.refs.code.focus() this.focus = () => this.refs.code.focus()
this.reload = () => this.refs.code.reload() this.reload = () => this.refs.code.reload()
this.userScroll = props.config.preview.scrollSync
this.state = {
isSliderFocused: false,
codeEditorWidthInPercent: 50,
codeEditorHeightInPercent: 50
}
} }
componentDidUpdate(prevProps) { handleOnChange () {
if (
this.props.config.preview.scrollSync !==
prevProps.config.preview.scrollSync
) {
this.userScroll = this.props.config.preview.scrollSync
}
}
handleCursorActivity(editor) {
if (this.userScroll) {
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const previewTop = _.get(previewDoc, 'body.scrollTop')
const line = editor.doc.getCursor().line
let top
if (line === 0) {
top = 0
} else {
const blockElements = previewDoc.querySelectorAll('body [data-line]')
const blocks = []
for (const block of blockElements) {
const l = parseInt(block.getAttribute('data-line'))
blocks.push({
line: l,
top: block.offsetTop
})
if (l > line) {
break
}
}
if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]
blocks.push({
line: editor.doc.size,
top: block.offsetTop + block.offsetHeight
})
}
const i = blocks.length - 1
const ratio =
(blocks[i].top - blocks[i - 1].top) /
(blocks[i].line - blocks[i - 1].line)
const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)
top =
blocks[i - 1].top +
Math.floor((line - blocks[i - 1].line) * ratio) -
delta
}
this.scrollTo(previewTop, top, y =>
_.set(previewDoc, 'body.scrollTop', y)
)
}
}
setValue(value) {
this.refs.code.setValue(value)
}
handleOnChange(e) {
this.value = this.refs.code.value this.value = this.refs.code.value
this.props.onChange(e) this.props.onChange()
} }
handleEditorScroll(e) { handleCheckboxClick (e) {
if (this.userScroll) {
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc')
const from = codeDoc.cm.coordsChar({ left: 0, top: 0 }).line
const to = codeDoc.cm.coordsChar({
left: 0,
top: codeDoc.cm.display.lastWrapHeight * 1.125
}).line
const previewTop = _.get(previewDoc, 'body.scrollTop')
let top
if (from === 0) {
top = 0
} else if (to === codeDoc.lastLine()) {
top =
_.get(previewDoc, 'body.scrollHeight') -
_.get(previewDoc, 'body.clientHeight')
} else {
const line = from + Math.floor((to - from) / 3)
const blockElements = previewDoc.querySelectorAll('body [data-line]')
const blocks = []
for (const block of blockElements) {
const l = parseInt(block.getAttribute('data-line'))
blocks.push({
line: l,
top: block.offsetTop
})
if (l > line) {
break
}
}
if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]
blocks.push({
line: codeDoc.size,
top: block.offsetTop + block.offsetHeight
})
}
const i = blocks.length - 1
const ratio =
(blocks[i].top - blocks[i - 1].top) /
(blocks[i].line - blocks[i - 1].line)
top =
blocks[i - 1].top + Math.floor((line - blocks[i - 1].line) * ratio)
}
this.scrollTo(previewTop, top, y =>
_.set(previewDoc, 'body.scrollTop', y)
)
}
}
handlePreviewScroll(e) {
if (this.userScroll) {
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc')
const srcTop = _.get(previewDoc, 'body.scrollTop')
const editorTop = _.get(codeDoc, 'scrollTop')
let top
if (srcTop === 0) {
top = 0
} else {
const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)
const previewTop = srcTop + delta
const blockElements = previewDoc.querySelectorAll('body [data-line]')
const blocks = []
for (const block of blockElements) {
const top = block.offsetTop
blocks.push({
line: parseInt(block.getAttribute('data-line')),
top
})
if (top > previewTop) {
break
}
}
if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]
blocks.push({
line: codeDoc.size,
top: block.offsetTop + block.offsetHeight
})
}
const i = blocks.length - 1
const from = codeDoc.cm.heightAtLine(blocks[i - 1].line, 'local')
const to = codeDoc.cm.heightAtLine(blocks[i].line, 'local')
const ratio =
(previewTop - blocks[i - 1].top) / (blocks[i].top - blocks[i - 1].top)
top = from + Math.floor((to - from) * ratio) - delta
}
this.scrollTo(editorTop, top, y => codeDoc.cm.scrollTo(0, y))
}
}
handleCheckboxClick(e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/ const idMatch = /checkbox-([0-9]+)/
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i const checkedMatch = /\[x\]/i
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/ const uncheckedMatch = /\[ \]/
const checkReplace = /\[x]/i
const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) { if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
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)) {
newLine = targetLine.replace(checkReplace, '[ ]') lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
} }
if (targetLine.match(uncheckedMatch)) { if (targetLine.match(uncheckedMatch)) {
newLine = targetLine.replace(uncheckReplace, '[x]') lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
} }
this.refs.code.setLineContent(lineIndex, newLine) this.refs.code.setValue(lines.join('\n'))
} }
} }
handleMouseMove(e) { render () {
if (this.state.isSliderFocused) { const { config, value, storageKey } = this.props
const rootRect = this.refs.root.getBoundingClientRect() const storage = findStorage(storageKey)
if (this.props.isStacking) {
const rootHeight = rootRect.height
const offset = rootRect.top
let newCodeEditorHeightInPercent =
((e.pageY - offset) / rootHeight) * 100
// limit minSize to 10%, maxSize to 90%
if (newCodeEditorHeightInPercent <= 10) {
newCodeEditorHeightInPercent = 10
}
if (newCodeEditorHeightInPercent >= 90) {
newCodeEditorHeightInPercent = 90
}
this.setState({
codeEditorHeightInPercent: newCodeEditorHeightInPercent
})
} else {
const rootWidth = rootRect.width
const offset = rootRect.left
let newCodeEditorWidthInPercent = ((e.pageX - offset) / rootWidth) * 100
// limit minSize to 10%, maxSize to 90%
if (newCodeEditorWidthInPercent <= 10) {
newCodeEditorWidthInPercent = 10
}
if (newCodeEditorWidthInPercent >= 90) {
newCodeEditorWidthInPercent = 90
}
this.setState({
codeEditorWidthInPercent: newCodeEditorWidthInPercent
})
}
}
}
handleMouseUp(e) {
e.preventDefault()
this.setState({
isSliderFocused: false
})
}
handleMouseDown(e) {
e.preventDefault()
this.setState({
isSliderFocused: true
})
}
scrollTo(from, to, scroller) {
const distance = to - from
const framerate = 1000 / 60
const frames = 20
const refractory = frames * framerate
this.userScroll = false
let frame = 0
let scrollPos, time
const timer = setInterval(() => {
time = frame / frames
scrollPos =
time < 0.5
? 2 * time * time // ease in
: -1 + (4 - 2 * time) * time // ease out
scroller(from + scrollPos * distance)
if (frame >= frames) {
clearInterval(timer)
setTimeout(() => {
this.userScroll = true
}, refractory)
}
frame++
}, framerate)
}
render() {
const {
config,
value,
storageKey,
noteKey,
linesHighlighted,
getNote,
isStacking,
RTL
} = this.props
let storage
try {
storage = findStorage(storageKey)
} catch (e) {
return <div />
}
let editorStyle = {}
let previewStyle = {}
let sliderStyle = {}
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
editorStyle.fontSize = editorFontSize
let editorIndentSize = parseInt(config.editor.indentSize, 10) let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorStyle.fontSize > 0 && editorStyle.fontSize < 132)) if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
editorIndentSize = 4 const previewStyle = {}
editorStyle.indentSize = editorIndentSize if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
editorStyle = Object.assign(
editorStyle,
isStacking
? {
width: '100%',
height: `${this.state.codeEditorHeightInPercent}%`
}
: {
width: `${this.state.codeEditorWidthInPercent}%`,
height: '100%'
}
)
previewStyle = Object.assign(
previewStyle,
isStacking
? {
width: '100%',
height: `${100 - this.state.codeEditorHeightInPercent}%`
}
: {
width: `${100 - this.state.codeEditorWidthInPercent}%`,
height: '100%'
}
)
sliderStyle = Object.assign(
sliderStyle,
isStacking
? {
left: 0,
top: `${this.state.codeEditorHeightInPercent}%`
}
: {
left: `${this.state.codeEditorWidthInPercent}%`,
top: 0
}
)
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused)
previewStyle.pointerEvents = 'none'
return ( return (
<div <div styleName='root'>
styleName='root'
ref='root'
onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}
>
<CodeEditor <CodeEditor
styleName='codeEditor'
ref='code' ref='code'
width={editorStyle.width} mode='GitHub Flavored Markdown'
height={editorStyle.height}
mode='Boost Flavored Markdown'
value={value} value={value}
theme={config.editor.theme} theme={config.editor.theme}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily} fontFamily={config.editor.fontFamily}
fontSize={editorStyle.fontSize} fontSize={editorFontSize}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping
matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorStyle.indentSize} indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
enableTableEditor={config.editor.enableTableEditor}
storageKey={storageKey} storageKey={storageKey}
noteKey={noteKey} onChange={this.handleOnChange.bind(this)}
linesHighlighted={linesHighlighted} />
onChange={e => this.handleOnChange(e)}
onScroll={e => this.handleEditorScroll(e)}
onCursorActivity={e => this.handleCursorActivity(e)}
spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
dateFormatISO8601={config.editor.dateFormatISO8601}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL}
/>
<div
styleName={isStacking ? 'slider-hoz' : 'slider'}
style={{ left: sliderStyle.left, top: sliderStyle.top }}
onMouseDown={e => this.handleMouseDown(e)}
>
<div styleName='slider-hitbox' />
</div>
<MarkdownPreview <MarkdownPreview
ref='preview'
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}
@@ -471,27 +79,14 @@ class MarkdownSplitEditor extends React.Component {
codeBlockTheme={config.preview.codeBlockTheme} codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily} codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
scrollPastEnd={config.preview.scrollPastEnd} scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes} ref='preview'
smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks}
sanitize={config.preview.sanitize}
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
tabInde='0' tabInde='0'
value={value} value={value}
onCheckboxClick={e => this.handleCheckboxClick(e)} onCheckboxClick={(e) => this.handleCheckboxClick(e)}
onScroll={e => this.handlePreviewScroll(e)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey} />
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
getNote={getNote}
export={config.export}
RTL={RTL}
/>
</div> </div>
) )
} }

View File

@@ -3,36 +3,7 @@
height 100% height 100%
font-size 30px font-size 30px
display flex display flex
flex-wrap wrap .codeEditor
.slider width 50%
absolute top bottom .preview
top -2px width 50%
width 0
z-index 0
border-left 1px solid $ui-borderColor
.slider-hitbox
absolute top bottom left right
width 7px
left -3px
z-index 10
cursor col-resize
.slider-hoz
absolute left right
.slider-hitbox
absolute left right
width: 100%
height 7px
cursor row-resize
apply-theme(theme)
body[data-theme={theme}]
.root
.slider
border-left 1px solid get-theme-var(theme, 'borderColor')
for theme in 'dark' 'dracula' 'solarized-dark'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -3,10 +3,12 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './ModalEscButton.styl' import styles from './ModalEscButton.styl'
const ModalEscButton = ({ handleEscButtonClick }) => ( const ModalEscButton = ({
handleEscButtonClick
}) => (
<button styleName='escButton' onClick={handleEscButtonClick}> <button styleName='escButton' onClick={handleEscButtonClick}>
<div styleName='esc-mark'>×</div> <div styleName='esc-mark'>×</div>
<div>esc</div> <div styleName='esc-text'>esc</div>
</button> </button>
) )

View File

@@ -1,23 +1,24 @@
/** /**
* @fileoverview Micro component for toggle SideNav * @fileoverview Micro component for toggle SideNav
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import styles from './NavToggleButton.styl' import styles from './NavToggleButton.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
/** /**
* @param {boolean} isFolded * @param {boolean} isFolded
* @param {Function} handleToggleButtonClick * @param {Function} handleToggleButtonClick
*/ */
const NavToggleButton = ({ isFolded, handleToggleButtonClick }) => ( const NavToggleButton = ({isFolded, handleToggleButtonClick}) => (
<button styleName='navToggle' onClick={e => handleToggleButtonClick(e)}> <button styleName='navToggle'
{isFolded ? ( onClick={(e) => handleToggleButtonClick(e)}
<i className='fa fa-angle-double-right fa-2x' /> >
) : ( {isFolded
<i className='fa fa-angle-double-left fa-2x' /> ? <i className='fa fa-angle-double-right' />
)} : <i className='fa fa-angle-double-left' />
}
</button> </button>
) )

View File

@@ -7,7 +7,7 @@
border-radius 16.5px border-radius 16.5px
height 34px height 34px
width 34px width 34px
line-height 100% line-height 32px
padding 0 padding 0
&:hover &:hover
border: 1px solid #1EC38B; border: 1px solid #1EC38B;
@@ -17,16 +17,10 @@
body[data-theme="white"] body[data-theme="white"]
navWhiteButtonColor() navWhiteButtonColor()
apply-theme(theme) body[data-theme="dark"]
body[data-theme={theme}] .navToggle
.navToggle:hover &:hover
background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%) background-color alpha($ui-dark-button--active-backgroundColor, 20%)
border 1px solid get-theme-var(theme, 'button--active-backgroundColor')
transition 0.15s transition 0.15s
color get-theme-var(theme, 'text-color') color $ui-dark-text-color
for theme in 'dark' 'dracula' 'solarized-dark'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -3,59 +3,38 @@
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { isArray, sortBy } from 'lodash' import { isArray } from 'lodash'
import invertColor from 'invert-color'
import Emoji from 'react-emoji-render'
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'
import TodoProcess from './TodoProcess' import TodoProcess from './TodoProcess'
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, color }) => { const TagElement = ({ tagName }) => (
const style = {} <span styleName='item-bottom-tagList-item' key={tagName}>
if (color) { #{tagName}
style.backgroundColor = color </span>
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 {Object} coloredTags
* @return {React.Component} * @return {React.Component}
*/ */
const TagElementList = (tags, showTagsAlphabetically, coloredTags) => { const TagElementList = (tags) => {
if (!isArray(tags)) { if (!isArray(tags)) {
return [] return []
} }
if (showTagsAlphabetically) { const tagElements = tags.map(tag => (
return sortBy(tags).map(tag => TagElement({tagName: tag})
TagElement({ tagName: tag, color: coloredTags[tag] }) ))
)
} else { return tagElements
return tags.map(tag =>
TagElement({ tagName: tag, color: coloredTags[tag] })
)
}
} }
/** /**
@@ -65,94 +44,48 @@ const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
* @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 = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
isActive, <div styleName={isActive
note, ? 'item--active'
dateDisplay, : 'item'
handleNoteClick, }
handleNoteContextMenu, key={`${note.storage}-${note.key}`}
handleDragStart, onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
pathname, onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
storageName,
folderName,
viewType,
showTagsAlphabetically,
coloredTags
}) => (
<div
styleName={isActive ? 'item--active' : 'item'}
key={note.key}
onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)}
onDragStart={e => handleDragStart(e, note)} onDragStart={e => handleDragStart(e, note)}
draggable='true' draggable='true'
> >
<div styleName='item-wrapper'> <div styleName='item-wrapper'>
{note.type === 'SNIPPET_NOTE' ? ( {note.type === 'SNIPPET_NOTE'
<i styleName='item-title-icon' className='fa fa-fw fa-code' /> ? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
) : ( : <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />
<i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' /> }
)}
<div styleName='item-title'> <div styleName='item-title'>
{note.title.trim().length > 0 ? ( {note.title.trim().length > 0
<Emoji text={note.title} /> ? note.title
) : ( : <span styleName='item-title-empty'>Empty</span>
<span styleName='item-title-empty'>{i18n.__('Empty note')}</span> }
)}
</div>
<div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div>
<div styleName='item-middle-app-meta'>
<div
title={
viewType === 'ALL'
? storageName
: viewType === 'STORAGE'
? folderName
: null
}
styleName='item-middle-app-meta-label'
>
{viewType === 'ALL' && storageName}
{viewType === 'STORAGE' && folderName}
</div>
</div>
</div> </div>
<div styleName='item-bottom-time'>{dateDisplay}</div>
{note.isStarred
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
}
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
}
{note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
: ''
}
<div styleName='item-bottom'> <div styleName='item-bottom'>
<div styleName='item-bottom-tagList'> <div styleName='item-bottom-tagList'>
{note.tags.length > 0 ? ( {note.tags.length > 0
TagElementList(note.tags, showTagsAlphabetically, coloredTags) ? TagElementList(note.tags)
) : ( : <span styleName='item-bottom-tagList-empty' />
<span }
style={{ fontStyle: 'italic', opacity: 0.5 }}
styleName='item-bottom-tagList-empty'
>
{i18n.__('No tags')}
</span>
)}
</div>
<div>
{note.isStarred ? (
<img
styleName='item-star'
src='../resources/icon/icon-starred.svg'
/>
) : (
''
)}
{note.isPinned && !pathname.match(/\/starred|\/trash/) ? (
<i styleName='item-pin' className='fa fa-thumb-tack' />
) : (
''
)}
{note.type === 'MARKDOWN_NOTE' ? (
<TodoProcess todoStatus={getTodoStatus(note.content)} />
) : (
''
)}
</div> </div>
</div> </div>
</div> </div>
@@ -162,7 +95,6 @@ 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,
@@ -170,15 +102,12 @@ NoteItem.propTypes = {
title: PropTypes.string.isrequired, title: PropTypes.string.isrequired,
tags: PropTypes.array, tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired, isStarred: PropTypes.bool.isRequired,
isTrashed: PropTypes.bool.isRequired, isTrashed: PropTypes.bool.isRequired
blog: PropTypes.shape({
blogLink: PropTypes.string,
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)

View File

@@ -90,26 +90,6 @@ $control-height = 30px
font-weight normal font-weight normal
color $ui-inactive-text-color color $ui-inactive-text-color
.item-middle
font-size 13px
padding-left 2px
padding-bottom 2px
.item-middle-time
color $ui-inactive-text-color
display inline-block
.item-middle-app-meta
float right
.item-middle-app-meta-label
opacity 0.4
color $ui-inactive-text-color
padding 0 3px
white-space nowrap
text-overflow ellipsis
overflow hidden
max-width 200px
.item-bottom .item-bottom
position relative position relative
bottom 0px bottom 0px
@@ -117,7 +97,7 @@ $control-height = 30px
font-size 12px font-size 12px
line-height 20px line-height 20px
overflow ellipsis overflow ellipsis
display block display flex
.item-bottom-tagList .item-bottom-tagList
flex 1 flex 1
@@ -145,8 +125,10 @@ $control-height = 30px
.item-star .item-star
position absolute position absolute
right 2px right -6px
top 5px bottom 23px
width 16px
height 16px
color alpha($ui-favorite-star-button-color, 60%) color alpha($ui-favorite-star-button-color, 60%)
font-size 12px font-size 12px
padding 0 padding 0
@@ -154,8 +136,10 @@ $control-height = 30px
.item-pin .item-pin
position absolute position absolute
right 25px right 0px
top 7px bottom 2px
width 34px
height 34px
color #E54D42 color #E54D42
font-size 14px font-size 14px
padding 0 padding 0
@@ -194,7 +178,7 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
.item-bottom-tagList-item .item-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha($ui-dark-tagList-backgroundColor, 20%) background-color alpha(#fff, 20%)
color $ui-dark-text-color color $ui-dark-text-color
&:active &:active
transition 0.15s transition 0.15s
@@ -207,8 +191,8 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
.item-bottom-tagList-item .item-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha($ui-dark-tagList-backgroundColor, 10%) background-color alpha(white, 10%)
color $ui-dark-text-color color $ui-dark-text-color
.item-wrapper .item-wrapper
border-color alpha($ui-dark-button--active-backgroundColor, 60%) border-color alpha($ui-dark-button--active-backgroundColor, 60%)
@@ -223,13 +207,13 @@ body[data-theme="dark"]
.item-bottom-time .item-bottom-time
color $ui-dark-text-color color $ui-dark-text-color
.item-bottom-tagList-item .item-bottom-tagList-item
background-color alpha($ui-dark-tagList-backgroundColor, 10%) background-color alpha(white, 10%)
color $ui-dark-text-color color $ui-dark-text-color
&:hover &:hover
background-color alpha($ui-dark-button--active-backgroundColor, 60%) background-color alpha($ui-dark-button--active-backgroundColor, 60%)
color $ui-dark-button--hover-color color #c0392b
.item-bottom-tagList-item .item-bottom-tagList-item
background-color alpha($ui-dark-tagList-backgroundColor, 20%) background-color alpha(#fff, 20%)
.item-title .item-title
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -282,7 +266,7 @@ body[data-theme="solarized-dark"]
.item-bottom-tagList-item .item-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 10%) background-color alpha($ui-solarized-dark-noteList-backgroundColor, 10%)
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
.item-wrapper .item-wrapper
border-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%) border-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
@@ -320,84 +304,4 @@ body[data-theme="solarized-dark"]
.item-bottom-tagList-empty .item-bottom-tagList-empty
color $ui-inactive-text-color color $ui-inactive-text-color
vertical-align middle vertical-align middle
apply-theme(theme)
body[data-theme={theme}]
.root
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteList-backgroundColor')
.item
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteList-backgroundColor')
&:hover
transition 0.15s
// background-color alpha(get-theme-var(theme, 'noteList-backgroundColor'), 20%)
color get-theme-var(theme, 'text-color')
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color get-theme-var(theme, 'text-color')
.item-bottom-tagList-item
transition 0.15s
background-color alpha(get-theme-var(theme, 'noteList-backgroundColor'), 20%)
color get-theme-var(theme, 'text-color')
&:active
transition 0.15s
background-color get-theme-var(theme, 'noteList-backgroundColor')
color get-theme-var(theme, 'text-color')
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color get-theme-var(theme, 'text-color')
.item-bottom-tagList-item
transition 0.15s
background-color alpha(get-theme-var(theme, 'noteList-backgroundColor'), 10%)
color get-theme-var(theme, 'text-color')
.item-wrapper
border-color alpha(get-theme-var(theme, 'button-backgroundColor'), 60%)
.item--active
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'button-backgroundColor')
.item-wrapper
border-color transparent
.item-title
.item-title-icon
.item-bottom-time
color get-theme-var(theme, 'active-color')
.item-bottom-tagList-item
background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 10%)
color get-theme-var(theme, 'text-color')
&:hover
// background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%)
color get-theme-var(theme, 'button--hover-color')
.item-bottom-tagList-item
background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 20%)
.item-title
color $ui-inactive-text-color
.item-title-icon
color $ui-inactive-text-color
.item-title-empty
color $ui-inactive-text-color
.item-bottom-tagList-item
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
color $ui-inactive-text-color
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle
for theme in 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -5,7 +5,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 './NoteItemSimple.styl' import styles from './NoteItemSimple.styl'
import i18n from 'browser/lib/i18n'
/** /**
* @description Note item component when using simple display mode. * @description Note item component when using simple display mode.
@@ -15,48 +14,30 @@ import i18n from 'browser/lib/i18n'
* @param {Function} handleNoteContextMenu * @param {Function} handleNoteContextMenu
* @param {Function} handleDragStart * @param {Function} handleDragStart
*/ */
const NoteItemSimple = ({ const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
isActive, <div styleName={isActive
isAllNotesView, ? 'item-simple--active'
note, : 'item-simple'
handleNoteClick, }
handleNoteContextMenu, key={`${note.storage}-${note.key}`}
handleDragStart, onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
pathname, onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
storage
}) => (
<div
styleName={isActive ? 'item-simple--active' : 'item-simple'}
key={note.key}
onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)}
onDragStart={e => handleDragStart(e, note)} onDragStart={e => handleDragStart(e, note)}
draggable='true' draggable='true'
> >
<div styleName='item-simple-title'> <div styleName='item-simple-title'>
{note.type === 'SNIPPET_NOTE' ? ( {note.type === 'SNIPPET_NOTE'
<i styleName='item-simple-title-icon' className='fa fa-fw fa-code' /> ? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
) : ( : <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
<i }
styleName='item-simple-title-icon' {note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
className='fa fa-fw fa-file-text-o' ? <i styleName='item-pin' className='fa fa-thumb-tack' />
/> : ''
)} }
{note.isPinned && !pathname.match(/\/starred|\/trash/) ? ( {note.title.trim().length > 0
<i styleName='item-pin' className='fa fa-thumb-tack' /> ? note.title
) : ( : <span styleName='item-simple-title-empty'>Empty</span>
'' }
)}
{note.title.trim().length > 0 ? (
note.title
) : (
<span styleName='item-simple-title-empty'>{i18n.__('Empty note')}</span>
)}
{isAllNotesView && (
<div styleName='item-simple-right'>
<span styleName='item-simple-right-storageName'>{storage.name}</span>
</div>
)}
</div> </div>
</div> </div>
) )

View File

@@ -104,7 +104,6 @@ body[data-theme="dark"]
background-color alpha($ui-dark-button--active-backgroundColor, 20%) background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-dark-text-color color $ui-dark-text-color
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
@@ -118,7 +117,6 @@ body[data-theme="dark"]
background-color $ui-dark-button--active-backgroundColor background-color $ui-dark-button--active-backgroundColor
color $ui-dark-text-color color $ui-dark-text-color
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
@@ -126,7 +124,7 @@ body[data-theme="dark"]
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha(white, 10%) background-color alpha(white, 10%)
color $ui-dark-text-color color $ui-dark-text-color
.item-simple--active .item-simple--active
border-color $ui-dark-borderColor border-color $ui-dark-borderColor
@@ -134,7 +132,6 @@ body[data-theme="dark"]
.item-simple-wrapper .item-simple-wrapper
border-color transparent border-color transparent
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
color $ui-dark-text-color color $ui-dark-text-color
@@ -168,10 +165,9 @@ body[data-theme="solarized-dark"]
background-color $ui-solarized-dark-noteList-backgroundColor background-color $ui-solarized-dark-noteList-backgroundColor
&:hover &:hover
transition 0.15s transition 0.15s
background-color alpha($ui-dark-button--active-backgroundColor, 60%) // background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
@@ -182,10 +178,9 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
&:active &:active
transition 0.15s transition 0.15s
// background-color $ui-solarized-dark-button--active-backgroundColor background-color $ui-solarized-dark-button--active-backgroundColor
color $ui-dark-text-color color $ui-solarized-dark-text-color
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
@@ -193,17 +188,15 @@ body[data-theme="solarized-dark"]
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha(white, 10%) background-color alpha(white, 10%)
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
.item-simple--active .item-simple--active
border-color $ui-solarized-dark-borderColor border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-tag-backgroundColor background-color $ui-solarized-dark-button--active-backgroundColor
.item-simple-wrapper .item-simple-wrapper
border-color transparent border-color transparent
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
color $ui-dark-text-color
.item-simple-bottom-time .item-simple-bottom-time
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
@@ -213,83 +206,4 @@ body[data-theme="solarized-dark"]
// background-color alpha($ui-dark-button--active-backgroundColor, 60%) // background-color alpha($ui-dark-button--active-backgroundColor, 60%)
color #c0392b color #c0392b
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
background-color alpha(#fff, 20%) background-color alpha(#fff, 20%)
.item-simple-title
color $ui-dark-text-color
border-bottom $ui-dark-borderColor
.item-simple-right
float right
.item-simple-right-storageName
padding-left 4px
opacity 0.4
apply-theme(theme)
body[data-theme={theme}]
.root
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteList-backgroundColor')
.item-simple
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteList-backgroundColor')
&:hover
transition 0.15s
background-color alpha(get-theme-var(theme, 'button-backgroundColor'), 60%)
color get-theme-var(theme, 'text-color')
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color get-theme-var(theme, 'text-color')
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 20%)
color get-theme-var(theme, 'text-color')
&:active
transition 0.15s
background-color get-theme-var(theme, 'button--active-backgroundColor')
color get-theme-var(theme, 'text-color')
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color get-theme-var(theme, 'text-color')
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 10%)
color get-theme-var(theme, 'text-color')
.item-simple--active
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'button--active-backgroundColor')
.item-simple-wrapper
border-color transparent
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
color get-theme-var(theme, 'text-color')
.item-simple-bottom-tagList-item
background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 10%)
color get-theme-var(theme, 'text-color')
&:hover
// background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%)
color #c0392b
.item-simple-bottom-tagList-item
background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 20%)
.item-simple-title
color $ui-dark-text-color
border-bottom $ui-dark-borderColor
.item-simple-right
float right
.item-simple-right-storageName
padding-left 4px
opacity 0.4
for theme in 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -6,7 +6,7 @@ const electron = require('electron')
const { shell } = electron const { shell } = electron
class RealtimeNotification extends React.Component { class RealtimeNotification extends React.Component {
constructor(props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
@@ -14,46 +14,38 @@ class RealtimeNotification extends React.Component {
} }
} }
componentDidMount() { componentDidMount () {
this.fetchNotifications() this.fetchNotifications()
} }
fetchNotifications() { fetchNotifications () {
const notificationsUrl = const notificationsUrl = 'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
fetch(notificationsUrl) fetch(notificationsUrl)
.then(response => { .then(response => {
return response.json() return response.json()
}) })
.then(json => { .then(json => {
this.setState({ notifications: json.notifications }) this.setState({notifications: json.notifications})
}) })
} }
handleLinkClick(e) { handleLinkClick (e) {
shell.openExternal(e.currentTarget.href) shell.openExternal(e.currentTarget.href)
e.preventDefault() e.preventDefault()
} }
render() { render () {
const { notifications } = this.state const { notifications } = this.state
const link = const link = notifications.length > 0
notifications.length > 0 ? ( ? <a styleName='notification-link' href={notifications[0].linkUrl}
<a onClick={(e) => this.handleLinkClick(e)}
styleName='notification-link' >
href={notifications[0].linkUrl} Info: {notifications[0].text}
onClick={e => this.handleLinkClick(e)} </a>
> : ''
Info: {notifications[0].text}
</a>
) : (
''
)
return ( return (
<div styleName='notification-area' style={this.props.style}> <div styleName='notification-area' style={this.props.style}>{link}</div>
{link}
</div>
) )
} }
} }

View File

@@ -30,20 +30,14 @@ body[data-theme="dark"]
&:hover &:hover
color #5CB85C color #5CB85C
apply-theme(theme)
body[data-theme={theme}]
.notification-area
background-color none
.notification-link body[data-theme="solarized-dark"]
color get-theme-var(theme, 'text-color') .notification-area
border none background-color none
background-color get-theme-var(theme, 'button-backgroundColor')
&:hover
color get-theme-var(theme, 'button--hover-color')
for theme in 'solarized-dark' 'dracula' .notification-link
apply-theme(theme) color $ui-solarized-dark-text-color
border none
for theme in $themes background-color $ui-solarized-dark-button-backgroundColor
apply-theme(theme) &:hover
color #5CB85C

View File

@@ -5,7 +5,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 './SideNavFilter.styl' import styles from './SideNavFilter.styl'
import i18n from 'browser/lib/i18n'
/** /**
* @param {boolean} isFolded * @param {boolean} isFolded
@@ -16,70 +15,54 @@ import i18n from 'browser/lib/i18n'
* @return {React.Component} * @return {React.Component}
*/ */
const SideNavFilter = ({ const SideNavFilter = ({
isFolded, isFolded, isHomeActive, handleAllNotesButtonClick,
isHomeActive, isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
handleAllNotesButtonClick, counterTotalNote, counterStarredNote
isStarredActive,
handleStarredButtonClick,
isTrashedActive,
handleTrashedButtonClick,
counterDelNote,
counterTotalNote,
counterStarredNote,
handleFilterButtonContextMenu
}) => ( }) => (
<div styleName={isFolded ? 'menu--folded' : 'menu'}> <div styleName={isFolded ? 'menu--folded' : 'menu'}>
<button
styleName={isHomeActive ? 'menu-button--active' : 'menu-button'} <button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
onClick={handleAllNotesButtonClick} onClick={handleAllNotesButtonClick}
> >
<div styleName='iconWrap'> <div styleName='iconWrap'>
<img <img src={isHomeActive
src={ ? '../resources/icon/icon-all-active.svg'
isHomeActive : '../resources/icon/icon-all.svg'
? '../resources/icon/icon-all-active.svg'
: '../resources/icon/icon-all.svg'
} }
/> />
</div> </div>
<span styleName='menu-button-label'>{i18n.__('All Notes')}</span> <span styleName='menu-button-label'>All Notes</span>
<span styleName='counters'>{counterTotalNote}</span> <span styleName='counters'>{counterTotalNote}</span>
</button> </button>
<button <button styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'}
styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'}
onClick={handleStarredButtonClick} onClick={handleStarredButtonClick}
> >
<div styleName='iconWrap'> <div styleName='iconWrap'>
<img <img src={isStarredActive
src={ ? '../resources/icon/icon-star-active.svg'
isStarredActive : '../resources/icon/icon-star-sidenav.svg'
? '../resources/icon/icon-star-active.svg'
: '../resources/icon/icon-star-sidenav.svg'
} }
/> />
</div> </div>
<span styleName='menu-button-label'>{i18n.__('Starred')}</span> <span styleName='menu-button-label'>Starred</span>
<span styleName='counters'>{counterStarredNote}</span> <span styleName='counters'>{counterStarredNote}</span>
</button> </button>
<button <button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
onClick={handleTrashedButtonClick} onClick={handleTrashedButtonClick}
onContextMenu={handleFilterButtonContextMenu}
> >
<div styleName='iconWrap'> <div styleName='iconWrap'>
<img <img src={isTrashedActive
src={ ? '../resources/icon/icon-trash-active.svg'
isTrashedActive : '../resources/icon/icon-trash-sidenav.svg'
? '../resources/icon/icon-trash-active.svg'
: '../resources/icon/icon-trash-sidenav.svg'
} }
/> />
</div> </div>
<span styleName='menu-button-label'>{i18n.__('Trash')}</span> <span styleName='menu-button-label'>Trash</span>
<span styleName='counters'>{counterDelNote}</span> <span styleName='counters'>{counterDelNote}</span>
</button> </button>
</div> </div>
) )
@@ -90,7 +73,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,
handleTrashedButtonClick: PropTypes.func.isRequired handleTrashdButtonClick: PropTypes.func.isRequired
} }
export default CSSModules(SideNavFilter, styles) export default CSSModules(SideNavFilter, styles)

View File

@@ -1,5 +1,5 @@
.menu .menu
margin-bottom 20px margin-bottom 30px
.menu-button .menu-button
navButtonColor() navButtonColor()
@@ -18,7 +18,7 @@
.iconWrap .iconWrap
width 20px width 20px
text-align center text-align center
.counters .counters
float right float right
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -68,9 +68,10 @@
.menu-button-label .menu-button-label
position fixed position fixed
display inline-block display inline-block
height 36px height 32px
left 44px left 44px
padding 0 10px padding 0 10px
margin-top -8px
margin-left 0 margin-left 0
overflow ellipsis overflow ellipsis
z-index 10 z-index 10
@@ -180,51 +181,45 @@ body[data-theme="dark"]
.menu-button-label .menu-button-label
color $ui-dark-text-color color $ui-dark-text-color
apply-theme(theme)
body[data-theme={theme}]
.menu-button
&:active
background-color get-theme-var(theme, 'noteList-backgroundColor')
color get-theme-var(theme, 'text-color')
&:hover
background-color get-theme-var(theme, 'button-backgroundColor')
color get-theme-var(theme, 'text-color')
.menu-button--active body[data-theme="solarized-dark"]
color get-theme-var(theme, 'text-color') .menu-button
background-color get-theme-var(theme, 'button-backgroundColor') &:active
background-color $ui-solarized-dark-noteList-backgroundColor
color $ui-solarized-dark-text-color
&:hover
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
.menu-button--active
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-button-backgroundColor
.menu-button-label
color $ui-solarized-dark-text-color
&:hover
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
.menu-button-label .menu-button-label
color get-theme-var(theme, 'text-color') color $ui-solarized-dark-text-color
&:hover
background-color get-theme-var(theme, 'button-backgroundColor')
color get-theme-var(theme, 'text-color')
.menu-button-label
color get-theme-var(theme, 'text-color')
.menu-button-star--active .menu-button-star--active
color get-theme-var(theme, 'text-color') color $ui-solarized-dark-text-color
background-color get-theme-var(theme, 'button-backgroundColor') background-color $ui-solarized-dark-button-backgroundColor
.menu-button-label
color $ui-solarized-dark-text-color
&:hover
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
.menu-button-label .menu-button-label
color get-theme-var(theme, 'text-color') color $ui-solarized-dark-text-color
&:hover
background-color get-theme-var(theme, 'button-backgroundColor')
color get-theme-var(theme, 'text-color')
.menu-button-label
color get-theme-var(theme, 'text-color')
.menu-button-trash--active .menu-button-trash--active
color get-theme-var(theme, 'text-color') color $ui-solarized-dark-text-color
background-color get-theme-var(theme, 'button-backgroundColor') background-color $ui-solarized-dark-button-backgroundColor
.menu-button-label
color $ui-solarized-dark-text-color
&:hover
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
.menu-button-label .menu-button-label
color get-theme-var(theme, 'text-color') color $ui-solarized-dark-text-color
&:hover
background-color get-theme-var(theme, 'button-backgroundColor')
color get-theme-var(theme, 'text-color')
.menu-button-label
color get-theme-var(theme, 'text-color')
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -2,10 +2,9 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetTab.styl' import styles from './SnippetTab.styl'
import context from 'browser/lib/context' import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
class SnippetTab extends React.Component { class SnippetTab extends React.Component {
constructor(props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
@@ -14,7 +13,7 @@ class SnippetTab extends React.Component {
} }
} }
componentWillUpdate(nextProps) { componentWillUpdate (nextProps) {
if (nextProps.snippet.name !== this.props.snippet.name) { if (nextProps.snippet.name !== this.props.snippet.name) {
this.setState({ this.setState({
name: nextProps.snippet.name name: nextProps.snippet.name
@@ -22,128 +21,125 @@ class SnippetTab extends React.Component {
} }
} }
handleClick(e) { handleClick (e) {
this.props.onClick(e) this.props.onClick(e)
} }
handleContextMenu(e) { handleContextMenu (e) {
context.popup([ context.popup([
{ {
label: i18n.__('Rename'), label: 'Rename',
click: e => this.handleRenameClick(e) click: (e) => this.handleRenameClick(e)
} }
]) ])
} }
handleRenameClick(e) { handleRenameClick (e) {
this.startRenaming() this.startRenaming()
} }
handleNameInputBlur(e) { handleNameInputBlur (e) {
this.handleRename() this.handleRename()
} }
handleNameInputChange(e) { handleNameInputChange (e) {
this.setState({ this.setState({
name: e.target.value name: e.target.value
}) })
} }
handleNameInputKeyDown(e) { handleNameInputKeyDown (e) {
switch (e.keyCode) { switch (e.keyCode) {
case 13: case 13:
this.handleRename() this.handleRename()
break break
case 27: case 27:
this.setState((prevState, props) => ({ this.setState({
name: props.snippet.name, name: this.props.snippet.name,
isRenaming: false isRenaming: false
})) })
break break
} }
} }
handleRename() { handleRename () {
this.setState( this.setState({
{ isRenaming: false
isRenaming: false }, () => {
}, if (this.props.snippet.name !== this.state.name) {
() => { this.props.onRename(this.state.name)
if (this.props.snippet.name !== this.state.name) {
this.props.onRename(this.state.name)
}
} }
) })
} }
handleDeleteButtonClick(e) { handleDeleteButtonClick (e) {
this.props.onDelete(e) this.props.onDelete(e)
} }
startRenaming() { startRenaming () {
this.setState( this.setState({
{ isRenaming: true
isRenaming: true }, () => {
}, this.refs.name.focus()
() => { this.refs.name.select()
this.refs.name.focus() })
this.refs.name.select()
}
)
} }
handleDragStart(e) { handleDragStart (e) {
e.dataTransfer.dropEffect = 'move' e.dataTransfer.dropEffect = 'move'
this.props.onDragStart(e) this.props.onDragStart(e)
} }
handleDrop(e) { handleDrop (e) {
this.props.onDrop(e) this.props.onDrop(e)
} }
render() { render () {
const { isActive, snippet, isDeletable } = this.props const { isActive, snippet, isDeletable } = this.props
return ( return (
<div styleName={isActive ? 'root--active' : 'root'}> <div styleName={isActive
{!this.state.isRenaming ? ( ? 'root--active'
<button : 'root'
styleName='button' }
onClick={e => this.handleClick(e)} >
onDoubleClick={e => this.handleRenameClick(e)} {!this.state.isRenaming
onContextMenu={e => this.handleContextMenu(e)} ? <button styleName='button'
onDragStart={e => this.handleDragStart(e)} onClick={(e) => this.handleClick(e)}
onDrop={e => this.handleDrop(e)} onDoubleClick={(e) => this.handleRenameClick(e)}
onContextMenu={(e) => this.handleContextMenu(e)}
onDragStart={(e) => this.handleDragStart(e)}
onDrop={(e) => this.handleDrop(e)}
draggable='true' draggable='true'
> >
{snippet.name.trim().length > 0 ? ( {snippet.name.trim().length > 0
snippet.name ? snippet.name
) : ( : <span styleName='button-unnamed'>
<span>{i18n.__('Unnamed')}</span> Unnamed
)} </span>
}
</button> </button>
) : ( : <input styleName='input'
<input
styleName='input'
ref='name' ref='name'
value={this.state.name} value={this.state.name}
onChange={e => this.handleNameInputChange(e)} onChange={(e) => this.handleNameInputChange(e)}
onBlur={e => this.handleNameInputBlur(e)} onBlur={(e) => this.handleNameInputBlur(e)}
onKeyDown={e => this.handleNameInputKeyDown(e)} onKeyDown={(e) => this.handleNameInputKeyDown(e)}
/> />
)} }
{isDeletable && ( {isDeletable &&
<button <button styleName='deleteButton'
styleName='deleteButton' onClick={(e) => this.handleDeleteButtonClick(e)}
onClick={e => this.handleDeleteButtonClick(e)}
> >
<i className='fa fa-times' /> <i className='fa fa-times' />
</button> </button>
)} }
</div> </div>
) )
} }
} }
SnippetTab.propTypes = {} SnippetTab.propTypes = {
}
export default CSSModules(SnippetTab, styles) export default CSSModules(SnippetTab, styles)

View File

@@ -1,44 +1,33 @@
.root .root
position relative position relative
flex 1 flex 1
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-text-color color $ui-inactive-text-color
visibility visible &:hover
transition 0.15s background-color darken($ui-backgroundColor, 15%)
.button &:active
color: $ui-text-color color white
transition 0.15s background-color $ui-active-color
.root--active .root--active
@extend .root @extend .root
min-width 100px min-width 100px
background-color alpha($ui-button--active-backgroundColor, 60%) border-bottom $ui-border
.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%
height 29px height 29px
overflow ellipsis overflow ellipsis
text-align left text-align left
padding-right 23px padding-right 30px
border none border none
background-color transparent background-color transparent
transition 0.15s transition 0.15s
border-left 4px solid transparent border-left 4px solid transparent
color $ui-inactive-text-color &:hover
background-color $ui-button--hover-backgroundColor
.deleteButton .deleteButton
position absolute position absolute
@@ -52,7 +41,6 @@
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
@@ -61,82 +49,90 @@
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 alpha($ui-dark-button--active-backgroundColor, 20%) background-color $ui-dark-button--hover-backgroundColor
transition 0.15s
.button
color $ui-dark-text-color
transition 0.15s
.deleteButton .deleteButton
color $ui-dark-text-color color $ui-dark-inactive-text-color
transition 0.15s &:hover
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
background-color $ui-dark-button--active-backgroundColor color $ui-dark-text-color
border-left 1px solid $ui-dark-borderColor border-color $ui-dark-borderColor
border-top 1px solid $ui-dark-borderColor &:hover
.button background-color $ui-dark-button--hover-backgroundColor
color $ui-dark-text-color .deleteButton
.deleteButton color $ui-dark-inactive-text-color
color $ui-dark-text-color &:hover
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--active-backgroundColor background-color $ui-dark-button--hover-backgroundColor
color $ui-dark-text-color color $ui-dark-text-color
transition 0.15s
apply-theme(theme) .deleteButton
body[data-theme={theme}] color alpha($ui-dark-text-color, 30%)
.root
border-color get-theme-var(theme, 'borderColor')
&:hover
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
transition 0.15s
.deleteButton
color get-theme-var(theme, 'text-color')
transition 0.15s
.button
color get-theme-var(theme, 'text-color')
transition 0.15s
.root--active body[data-theme="solarized-dark"]
color get-theme-var(theme, 'active-color') .root
background-color get-theme-var(theme, 'button-backgroundColor') color $ui-solarized-dark-text-color
border-color get-theme-var(theme, 'borderColor') border-color $ui-dark-borderColor
&:hover
background-color $ui-solarized-dark-noteDetail-backgroundColor
.deleteButton .deleteButton
color get-theme-var(theme, 'text-color') color $ui-solarized-dark-text-color
.button &:hover
color get-theme-var(theme, 'active-color') background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
&:active
color $ui-solarized-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.button .root--active
border none color $ui-solarized-dark-text-color
color $ui-inactive-text-color border-color $ui-solarized-dark-borderColor
background-color transparent &:hover
transition color background-color 0.15s background-color $ui-solarized-dark-noteDetail-backgroundColor
border-left 4px solid transparent .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
.input .button
background-color get-theme-var(theme, 'noteDetail-backgroundColor') border none
color get-theme-var(theme, 'text-color') color $ui-solarized-dark-text-color
transition 0.15s background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-noteDetail-backgroundColor
for theme in 'solarized-dark' 'dracula' .input
apply-theme(theme) background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color
for theme in $themes .deleteButton
apply-theme(theme) color alpha($ui-solarized-dark-text-color, 30%)

View File

@@ -6,24 +6,10 @@ import React from 'react'
import styles from './StorageItem.styl' import styles from './StorageItem.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import _ from 'lodash' import _ from 'lodash'
import { SortableHandle } from 'react-sortable-hoc'
const DraggableIcon = SortableHandle(({ className }) => (
<i className={`fa ${className}`} />
))
const FolderIcon = ({ className, color, isActive }) => {
const iconStyle = isActive ? 'fa-folder-open-o' : 'fa-folder-o'
return (
<i className={`fa ${iconStyle} ${className}`} style={{ color: color }} />
)
}
/** /**
* @param {boolean} isActive * @param {boolean} isActive
* @param {object} tooltipRef,
* @param {Function} handleButtonClick * @param {Function} handleButtonClick
* @param {Function} handleMouseEnter
* @param {Function} handleContextMenu * @param {Function} handleContextMenu
* @param {string} folderName * @param {string} folderName
* @param {string} folderColor * @param {string} folderColor
@@ -35,64 +21,38 @@ const FolderIcon = ({ className, color, isActive }) => {
* @return {React.Component} * @return {React.Component}
*/ */
const StorageItem = ({ const StorageItem = ({
styles, isActive, handleButtonClick, handleContextMenu, folderName,
isActive, folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave
tooltipRef, }) => (
handleButtonClick, <button styleName={isActive
handleMouseEnter, ? 'folderList-item--active'
handleContextMenu, : 'folderList-item'
folderName, }
folderColor, onClick={handleButtonClick}
isFolded, onContextMenu={handleContextMenu}
noteCount, onDrop={handleDrop}
handleDrop, onDragEnter={handleDragEnter}
handleDragEnter, onDragLeave={handleDragLeave}
handleDragLeave >
}) => { <span styleName={isFolded
return ( ? 'folderList-item-name--folded' : 'folderList-item-name'
<button }>
styleName={isActive ? 'folderList-item--active' : 'folderList-item'} <text style={{color: folderColor, paddingRight: '10px'}}>{isActive ? <i className='fa fa-folder-open-o' /> : <i className='fa fa-folder-o' />}</text>{isFolded ? _.truncate(folderName, {length: 1, omission: ''}) : folderName}
onClick={handleButtonClick} </span>
onMouseEnter={handleMouseEnter} {(!isFolded && _.isNumber(noteCount)) &&
onContextMenu={handleContextMenu} <span styleName='folderList-item-noteCount'>{noteCount}</span>
onDrop={handleDrop} }
onDragEnter={handleDragEnter} {isFolded &&
onDragLeave={handleDragLeave} <span styleName='folderList-item-tooltip'>
> {folderName}
{!isFolded && (
<DraggableIcon className={styles['folderList-item-reorder']} />
)}
<span
styleName={
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
}
>
<FolderIcon
styleName='folderList-item-icon'
color={folderColor}
isActive={isActive}
/>
{isFolded
? _.truncate(folderName, { length: 1, omission: '' })
: folderName}
</span> </span>
{!isFolded && _.isNumber(noteCount) && ( }
<span styleName='folderList-item-noteCount'>{noteCount}</span> </button>
)} )
{isFolded && (
<span styleName='folderList-item-tooltip' ref={tooltipRef}>
{folderName}
</span>
)}
</button>
)
}
StorageItem.propTypes = { StorageItem.propTypes = {
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
tooltipRef: PropTypes.object,
handleButtonClick: PropTypes.func, handleButtonClick: PropTypes.func,
handleMouseEnter: PropTypes.func,
handleContextMenu: PropTypes.func, handleContextMenu: PropTypes.func,
folderName: PropTypes.string.isRequired, folderName: PropTypes.string.isRequired,
folderColor: PropTypes.string, folderColor: PropTypes.string,

View File

@@ -13,7 +13,6 @@
border none border none
overflow ellipsis overflow ellipsis
font-size 14px font-size 14px
align-items: center
&:first-child &:first-child
margin-top 0 margin-top 0
&:hover &:hover
@@ -23,7 +22,7 @@
&:active &:active
color $$ui-button-default-color color $$ui-button-default-color
background-color alpha($ui-button-default--active-backgroundColor, 20%) background-color alpha($ui-button-default--active-backgroundColor, 20%)
.folderList-item--active .folderList-item--active
@extend .folderList-item @extend .folderList-item
color #1EC38B color #1EC38B
@@ -35,7 +34,9 @@
.folderList-item-name .folderList-item-name
display block display block
flex 1 flex 1
padding-right: 10px padding 0 12px
height 26px
line-height 26px
border-width 0 0 0 2px border-width 0 0 0 2px
border-style solid border-style solid
border-color transparent border-color transparent
@@ -58,9 +59,8 @@
opacity 0 opacity 0
border-top-right-radius 2px border-top-right-radius 2px
border-bottom-right-radius 2px border-bottom-right-radius 2px
height 34px height 26px
line-height 32px line-height 26px
transition-property opacity
.folderList-item:hover, .folderList-item--active:hover .folderList-item:hover, .folderList-item--active:hover
.folderList-item-tooltip .folderList-item-tooltip
@@ -69,20 +69,9 @@
.folderList-item-name--folded .folderList-item-name--folded
@extend .folderList-item-name @extend .folderList-item-name
padding-left 7px padding-left 7px
.folderList-item-icon text
font-size 9px font-size 9px
.folderList-item-icon
padding-right: 10px
.folderList-item-reorder
font-size: 9px
padding: 10px 8px 10px 9px;
color: rgba(147, 147, 149, 0.3)
cursor: ns-resize
&:before
content: "\f142 \f142"
body[data-theme="white"] body[data-theme="white"]
.folderList-item .folderList-item
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -121,28 +110,21 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
background-color alpha($ui-dark-button--active-backgroundColor, 50%) background-color alpha($ui-dark-button--active-backgroundColor, 50%)
apply-theme(theme) body[data-theme="solarized-dark"]
body[data-theme={theme}] .folderList-item
.folderList-item &:hover
&:hover background-color $ui-solarized-dark-button-backgroundColor
background-color get-theme-var(theme, 'button-backgroundColor') color $ui-solarized-dark-text-color
color get-theme-var(theme, 'text-color') &:active
&:active color $ui-solarized-dark-text-color
color get-theme-var(theme, 'text-color') background-color $ui-solarized-dark-button-backgroundColor
background-color get-theme-var(theme, 'button-backgroundColor')
.folderList-item--active .folderList-item--active
@extend .folderList-item @extend .folderList-item
color get-theme-var(theme, 'text-color') color $ui-solarized-dark-text-color
background-color get-theme-var(theme, 'button-backgroundColor') background-color $ui-solarized-dark-button-backgroundColor
&:active &:active
background-color get-theme-var(theme, 'button-backgroundColor') background-color $ui-solarized-dark-button-backgroundColor
&:hover &:hover
color get-theme-var(theme, 'text-color') color $ui-solarized-dark-text-color
background-color get-theme-var(theme, 'button-backgroundColor') background-color $ui-solarized-dark-button-backgroundColor
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -1,26 +1,24 @@
/** /**
* @fileoverview Micro component for showing StorageList * @fileoverview Micro component for showing StorageList
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import styles from './StorageList.styl' import styles from './StorageList.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
/** /**
* @param {Array} storageList * @param {Array} storgaeList
*/ */
const StorageList = ({ storageList, isFolded }) => ( const StorageList = ({storageList}) => (
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}> <div styleName='storageList'>
{storageList.length > 0 ? ( {storageList.length > 0 ? storageList : (
storageList <div styleName='storgaeList-empty'>No storage mount.</div>
) : (
<div styleName='storageList-empty'>No storage mount.</div>
)} )}
</div> </div>
) )
StorageList.propTypes = { StorageList.propTypes = {
storageList: PropTypes.arrayOf(PropTypes.element).isRequired storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired
} }
export default CSSModules(StorageList, styles) export default CSSModules(StorageList, styles)

View File

@@ -4,10 +4,6 @@
top 180px top 180px
overflow-y auto overflow-y auto
.storageList-folded
@extend .storageList
width 44px
.storageList-empty .storageList-empty
padding 0 10px padding 0 10px
margin-top 15px margin-top 15px

View File

@@ -1,70 +1,28 @@
/** /**
* @fileoverview Micro component for showing TagList. * @fileoverview Micro component for showing TagList.
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import styles from './TagListItem.styl' import styles from './TagListItem.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
/** /**
* @param {string} name * @param {string} name
* @param {Function} handleClickTagListItem * @param {Function} handleClickTagListItem
* @param {Function} handleClickNarrowToTag * @param {bool} isActive
* @param {boolean} isActive */
* @param {boolean} isRelated
* @param {string} bgColor tab backgroundColor
*/
const TagListItem = ({ const TagListItem = ({name, handleClickTagListItem, isActive}) => (
name, <button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
handleClickTagListItem, <span styleName='tagList-item-name'>
handleClickNarrowToTag, {`# ${name}`}
handleContextMenu, </span>
isActive, </button>
isRelated,
count,
color
}) => (
<div
styleName='tagList-itemContainer'
onContextMenu={e => handleContextMenu(e, name)}
>
{isRelated ? (
<button
styleName={
isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'
}
onClick={() => handleClickNarrowToTag(name)}
>
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
</button>
) : (
<div
styleName={
isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'
}
/>
)}
<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'>
{`# ${name}`}
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
</span>
</button>
</div>
) )
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)

View File

@@ -1,9 +1,5 @@
.tagList-itemContainer
display flex
.tagList-item .tagList-item
display flex display flex
flex 1
width 100% width 100%
height 26px height 26px
background-color transparent background-color transparent
@@ -24,16 +20,9 @@
color $ui-button-default-color color $ui-button-default-color
background-color $ui-button-default--active-backgroundColor background-color $ui-button-default--active-backgroundColor
.tagList-itemNarrow
composes tagList-item
flex none
width 20px
padding 0 4px
.tagList-item-active .tagList-item-active
background-color $ui-button-default--active-backgroundColor background-color $ui-button-default--active-backgroundColor
display flex display flex
flex 1
width 100% width 100%
height 26px height 26px
padding 0 padding 0
@@ -47,16 +36,10 @@
background-color alpha($ui-button-default--active-backgroundColor, 60%) background-color alpha($ui-button-default--active-backgroundColor, 60%)
transition 0.2s transition 0.2s
.tagList-itemNarrow-active
composes tagList-item-active
flex none
width 20px
padding 0 4px
.tagList-item-name .tagList-item-name
display block display block
flex 1 flex 1
padding 0 8px 0 4px padding 0 15px
height 26px height 26px
line-height 26px line-height 26px
border-width 0 0 0 2px border-width 0 0 0 2px
@@ -65,17 +48,6 @@
overflow hidden overflow hidden
text-overflow ellipsis text-overflow ellipsis
.tagList-item-count
float right
line-height 26px
padding-right 15px
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
@@ -91,33 +63,22 @@ body[data-theme="white"]
color $ui-text-color color $ui-text-color
&:hover &:hover
background-color alpha($ui-button--active-backgroundColor, 60%) background-color alpha($ui-button--active-backgroundColor, 60%)
.tagList-item-count
color $ui-text-color
apply-theme(theme) body[data-theme="dark"]
body[data-theme={theme}] .tagList-item
.tagList-item color $ui-dark-inactive-text-color
color get-theme-var(theme, 'inactive-text-color') &:hover
&:hover color $ui-dark-text-color
color get-theme-var(theme, 'text-color') background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%) &:active
&:active color $ui-dark-text-color
color get-theme-var(theme, 'text-color') background-color $ui-dark-button--active-backgroundColor
background-color get-theme-var(theme, 'button--active-backgroundColor')
.tagList-item-active .tagList-item-active
background-color get-theme-var(theme, 'button--active-backgroundColor') background-color $ui-dark-button--active-backgroundColor
color get-theme-var(theme, 'text-color') color $ui-dark-text-color
&:active &:active
background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 50%) background-color alpha($ui-dark-button--active-backgroundColor, 50%)
&:hover &:hover
color get-theme-var(theme, 'text-color') color $ui-dark-text-color
background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 50%) background-color alpha($ui-dark-button--active-backgroundColor, 50%)
.tagList-item-count
color get-theme-var(theme, 'button--active-color')
for theme in 'dark'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -11,27 +11,20 @@ import styles from './TodoListPercentage.styl'
* @param {number} percentageOfTodo * @param {number} percentageOfTodo
*/ */
const TodoListPercentage = ({ percentageOfTodo, onClearCheckboxClick }) => ( const TodoListPercentage = ({
<div percentageOfTodo
styleName='percentageBar' }) => (
style={{ display: isNaN(percentageOfTodo) ? 'none' : '' }} <div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
> <div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
<div styleName='progressBar' style={{ width: `${percentageOfTodo}%` }}>
<div styleName='progressBarInner'> <div styleName='progressBarInner'>
<p styleName='percentageText'>{percentageOfTodo}%</p> <p styleName='percentageText'>{percentageOfTodo}%</p>
</div> </div>
</div> </div>
<div styleName='todoClear'>
<p styleName='todoClearText' onClick={e => onClearCheckboxClick(e)}>
clear
</p>
</div>
</div> </div>
) )
TodoListPercentage.propTypes = { TodoListPercentage.propTypes = {
percentageOfTodo: PropTypes.number.isRequired, percentageOfTodo: PropTypes.number.isRequired
onClearCheckboxClick: PropTypes.func.isRequired
} }
export default CSSModules(TodoListPercentage, styles) export default CSSModules(TodoListPercentage, styles)

View File

@@ -1,5 +1,4 @@
.percentageBar .percentageBar
display: flex
position absolute position absolute
top 72px top 72px
right 0px right 0px
@@ -31,20 +30,6 @@
color #f4f4f4 color #f4f4f4
font-weight 600 font-weight 600
.todoClear
display flex
justify-content: flex-end
position absolute
z-index 120
width 100%
height 100%
padding 2px 10px
.todoClearText
color #f4f4f4
cursor pointer
font-weight 500
body[data-theme="dark"] body[data-theme="dark"]
.percentageBar .percentageBar
background-color #444444 background-color #444444
@@ -54,10 +39,7 @@ body[data-theme="dark"]
.percentageText .percentageText
color $ui-dark-text-color color $ui-dark-text-color
.todoClearText
color $ui-dark-text-color
body[data-theme="solarized-dark"] body[data-theme="solarized-dark"]
.percentageBar .percentageBar
background-color #002b36 background-color #002b36
@@ -66,24 +48,4 @@ body[data-theme="solarized-dark"]
background-color: #2aa198 background-color: #2aa198
.percentageText .percentageText
color #fdf6e3 color #fdf6e3
.todoClearText
color #fdf6e3
apply-theme(theme)
body[data-theme={theme}]
.percentageBar
background-color: get-theme-var(theme, 'borderColor')
.progressBar
background-color get-theme-var(theme, 'active-color')
.percentageText
color get-theme-var(theme, 'text-color')
for theme in 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -8,30 +8,27 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TodoProcess.styl' import styles from './TodoProcess.styl'
const TodoProcess = ({ const TodoProcess = ({
todoStatus: { total: totalTodo, completed: completedTodo } todoStatus: {
total: totalTodo,
completed: completedTodo
}
}) => ( }) => (
<div <div styleName='todo-process' style={{display: totalTodo > 0 ? '' : 'none'}}>
styleName='todo-process'
style={{ display: totalTodo > 0 ? '' : 'none' }}
>
<div styleName='todo-process-text'> <div styleName='todo-process-text'>
<i className='fa fa-fw fa-check-square-o' /> <i className='fa fa-fw fa-check-square-o' />
{completedTodo} of {totalTodo} {completedTodo} of {totalTodo}
</div> </div>
<div styleName='todo-process-bar'> <div styleName='todo-process-bar'>
<div <div styleName='todo-process-bar--inner' style={{width: parseInt(completedTodo / totalTodo * 100) + '%'}} />
styleName='todo-process-bar--inner'
style={{ width: parseInt((completedTodo / totalTodo) * 100) + '%' }}
/>
</div> </div>
</div> </div>
) )
TodoProcess.propTypes = { TodoProcess.propTypes = {
todoStatus: PropTypes.exact({ todoStatus: {
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)

View File

@@ -55,14 +55,11 @@ 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
white-space initial font 400 1.2em 'KaTeX_Main'
.katex .katex-html line-height 1.2em
display inline-flex white-space nowrap
text-indent 0
.katex .mfrac>.vlist>span:nth-child(2) .katex .mfrac>.vlist>span:nth-child(2)
top 0 !important top 0 !important
.katex-error .katex-error
@@ -71,7 +68,7 @@ body
padding 5px padding 5px
margin -5px margin -5px
border-radius 5px border-radius 5px
.flowchart-error, .sequence-error .chart-error .flowchart-error, .sequence-error
background-color errorBackgroundColor background-color errorBackgroundColor
color errorTextColor color errorTextColor
padding 5px padding 5px
@@ -79,13 +76,10 @@ body
justify-content left justify-content left
li li
label.taskListItem label.taskListItem
margin-left -1.8em margin-left -2em
&.checked &.checked
text-decoration line-through text-decoration line-through
opacity 0.5 opacity 0.5
&.taskListItem.checked
text-decoration line-through
opacity 0.5
div.math-rendered div.math-rendered
text-align center text-align center
.math-failed .math-failed
@@ -124,34 +118,40 @@ hr
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
margin 15px 0 margin 15px 0
h1, h2, h3, h4, h5, h6 h1, h2, h3, h4, h5, h6
margin 1em 0 1.5em
line-height 1.4
font-weight bold font-weight bold
word-wrap break-word word-wrap break-word
padding .2em 0 .2em
h1 h1
font-size 2.55em font-size 2.55em
line-height 1.2 padding-bottom 0.3em
line-height 1.2em
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
margin 1em 0 0.44em
&:first-child &:first-child
margin-top 0 margin-top 0
h2 h2
font-size 1.75em font-size 1.75em
line-height 1.225 padding-bottom 0.3em
line-height 1.225em
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
margin 1em 0 0.57em
&:first-child &:first-child
margin-top 0 margin-top 0
h3 h3
font-size 1.5em font-size 1.5em
line-height 1.43 line-height 1.43em
margin 1em 0 0.66em
h4 h4
font-size 1.25em font-size 1.25em
line-height 1.4 line-height 1.4em
margin 1em 0 0.8em
h5 h5
font-size 1em font-size 1em
line-height 1.1 line-height 1.4em
margin 1em 0 1em
h6 h6
font-size 1em font-size 1em
line-height 1.4em
margin 1em 0 1em
color #777 color #777
p p
line-height 1.6em line-height 1.6em
@@ -159,7 +159,6 @@ 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
@@ -179,12 +178,6 @@ ul
margin-bottom 1em margin-bottom 1em
li li
display list-item display list-item
&.taskListItem
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
@@ -204,6 +197,7 @@ ol
&>li>ul, &>li>ol &>li>ul, &>li>ol
margin 0 margin 0
code code
color #CC305F
padding 0.2em 0.4em padding 0.2em 0.4em
background-color #f7f7f7 background-color #f7f7f7
border-radius 3px border-radius 3px
@@ -211,39 +205,33 @@ code
text-decoration none text-decoration none
margin-right 2px margin-right 2px
pre pre
padding 0.5rem !important padding 0.5em !important
border solid 1px #D1D1D1 border solid 1px #D1D1D1
border-radius 5px border-radius 5px
overflow-x auto overflow-x auto
margin 0 0 1rem margin 0 0 1em
display flex display flex
line-height 1.4em line-height 1.4em
&.flowchart, &.sequence
display flex
justify-content center
background-color white
&.CodeMirror
height initial
&>code
flex 1
overflow-x auto
code code
background-color inherit background-color inherit
margin 0 margin 0
padding 0 padding 0
border none border none
border-radius 0 border-radius 0
&.CodeMirror
height initial
flex-wrap wrap
&>code
flex 1
overflow-x auto
&.mermaid svg
max-width 100% !important
&>span.filename
margin -0.5rem 100% 0.5rem -0.5rem
padding 0.125rem 0.375rem
background-color #777;
color white
&:empty
display none
&>span.lineNumber &>span.lineNumber
display none display none
font-size 1em font-size 1em
padding 0.5rem 0 padding 0.5em 0
margin -0.5rem 0.5rem -0.5rem -0.5rem margin -0.5em 0.5em -0.5em -0.5em
border-right 1px solid border-right 1px solid
text-align right text-align right
border-top-left-radius 4px border-top-left-radius 4px
@@ -260,7 +248,6 @@ table
display block display block
width 100% width 100%
margin 0 0 1em margin 0 0 1em
overflow-x auto
thead thead
tr tr
background-color tableHeadBgColor background-color tableHeadBgColor
@@ -297,191 +284,6 @@ kbd
line-height 1 line-height 1
padding 3px 5px padding 3px 5px
$admonition
box-shadow 0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)
position relative
margin 1.5625em 0
padding 0 1.2rem
border-left .4rem solid #448aff
border-radius .2rem
overflow auto
html .admonition>:last-child
margin-bottom 1.2rem
.admonition .admonition
margin 1em 0
.admonition p
margin-top: 0.5em
$admonition-icon
position absolute
left 1.2rem
font-family: "Material Icons"
font-weight: normal;
font-style: normal;
font-size: 24px
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
$admonition-title
margin 0 -1.2rem
padding .8rem 1.2rem .8rem 4rem
border-bottom .1rem solid rgba(68,138,255,.1)
background-color rgba(68,138,255,.1)
font-weight 700
.admonition>.admonition-title:last-child
margin-bottom 0
admonition_types = {
note: {color: #0288D1, icon: "note"},
hint: {color: #009688, icon: "info_outline"},
danger: {color: #c2185b, icon: "block"},
caution: {color: #ffa726, icon: "warning"},
error: {color: #d32f2f, icon: "error_outline"},
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
.admonition.{name}
@extend $admonition
border-left-color: val[color]
.admonition.{name}>.admonition-title
@extend $admonition-title
border-bottom-color: .1rem solid rgba(val[color], 0.2)
background-color: rgba(val[color], 0.2)
.admonition.{name}>.admonition-title:before
@extend $admonition-icon
color: val[color]
content: val[icon]
dl
margin 2rem 0
padding 0
display flex
width 100%
flex-wrap wrap
align-items flex-start
border-bottom 1px solid borderColor
background-color tableHeadBgColor
dt
border-top 1px solid borderColor
font-weight bold
text-align right
overflow hidden
flex-basis 20%
padding 0.4rem 0.9rem
box-sizing border-box
dd
border-top 1px solid borderColor
flex-basis 80%
padding 0.4rem 0.9rem
min-height 2.5rem
background-color $ui-noteDetail-backgroundColor
box-sizing border-box
dd + dd
margin-left 20%
pre.fence
flex-wrap wrap
.chart, .flowchart, .mermaid, .sequence
display flex
justify-content center
background-color white
max-width 100%
flex-grow 1
canvas, svg
max-width 100% !important
svg[ratio]
width 100%
.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
.markdownIt-TOC-wrapper
list-style none
position fixed
right 0
top 0
margin-left 15px
z-index 1000
transition transform .2s ease-in-out
transform translateX(100%)
.markdownIt-TOC
display block
max-height 90vh
overflow-y auto
padding 25px
padding-left 38px
&,
&:before
background-color $ui-dark-backgroundColor
color: $ui-dark-text-color
&:hover
transform translateX(-15px)
&:before
content 'TOC'
position absolute
width 60px
height 30px
top 60px
left -29px
display flex
align-items center
justify-content center
transform-origin top left
transform rotate(-90deg)
themeDarkBackground = darken(#21252B, 10%) themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9 themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%) themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -532,80 +334,30 @@ body[data-theme="dark"]
kbd kbd
background-color themeDarkBorder background-color themeDarkBorder
color themeDarkText color themeDarkText
dl
border-color themeDarkBorder
background-color themeDarkTableHead
dt
border-color themeDarkBorder
dd
border-color themeDarkBorder
background-color themeDarkPreview
pre.fence themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
.gallery themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
.carousel-main, .carousel-footer themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
background-color $ui-dark-noteDetail-backgroundColor themeSolarizedDarkTableBorder = themeDarkBorder
.prev, .next
color $ui-dark-text-color
background-color $ui-dark-tag-backgroundColor
.markdownIt-TOC-wrapper body[data-theme="solarized-dark"]
&, color $ui-solarized-dark-text-color
&:before border-color themeDarkBorder
background-color darken(themeDarkBackground, 5%) background-color $ui-solarized-dark-noteDetail-backgroundColor
color themeDarkText table
thead
apply-theme(theme) tr
body[data-theme={theme}] background-color themeSolarizedDarkTableHead
color get-theme-var(theme, 'text-color') th
border-color themeDarkBorder border-color themeSolarizedDarkTableBorder
background-color get-theme-var(theme, 'noteDetail-backgroundColor') &:last-child
table border-right solid 1px themeSolarizedDarkTableBorder
thead tbody
tr tr:nth-child(2n + 1)
background-color get-theme-var(theme, 'table-head-backgroundColor') background-color themeSolarizedDarkTableOdd
th tr:nth-child(2n)
border-color get-theme-var(theme, 'table-borderColor') background-color themeSolarizedDarkTableEven
&:last-child td
border-right solid 1px get-theme-var(theme, 'table-borderColor') border-color themeSolarizedDarkTableBorder
tbody &:last-child
tr:nth-child(2n + 1) border-right solid 1px themeSolarizedDarkTableBorder
background-color get-theme-var(theme, 'table-odd-backgroundColor')
tr:nth-child(2n)
background-color get-theme-var(theme, 'table-even-backgroundColor')
td
border-color get-theme-var(theme, 'table-borderColor')
&:last-child
border-right solid 1px get-theme-var(theme, 'table-borderColor')
kbd
background-color get-theme-var(theme, 'kbd-backgroundColor')
color get-theme-var(theme, 'kbd-color')
dl
border-color themeDarkBorder
background-color get-theme-var(theme, 'table-head-backgroundColor')
dt
border-color themeDarkBorder
dd
border-color themeDarkBorder
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
.prev, .next
color get-theme-var(theme, 'button--active-color')
background-color get-theme-var(theme, 'button-backgroundColor')
.markdownIt-TOC-wrapper
&,
&:before
background-color darken(get-theme-var(theme, 'noteDetail-backgroundColor'), 15%)
color themeDarkText
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -1,72 +0,0 @@
import mermaidAPI from 'mermaid/dist/mermaid.min.js'
import uiThemes from 'browser/lib/ui-themes'
// fixes bad styling in the mermaid dark theme
const darkThemeStyling = `
.loopText tspan {
fill: white;
}`
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min
}
function getId() {
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let id = 'm-'
for (let i = 0; i < 7; i++) {
id += pool[getRandomInt(0, 16)]
}
return id
}
function render(element, content, theme, enableHTMLLabel) {
try {
const height = element.attributes.getNamedItem('data-height')
const isPredefined = height && height.value !== 'undefined'
if (isPredefined) {
element.style.height = height.value + 'vh'
}
const isDarkTheme = uiThemes.some(
item => item.name === theme && item.isDark
)
mermaidAPI.initialize({
theme: isDarkTheme ? 'dark' : 'default',
themeCSS: isDarkTheme ? darkThemeStyling : '',
flowchart: {
htmlLabels: enableHTMLLabel
},
gantt: {
useWidth: element.clientWidth
}
})
mermaidAPI.render(getId(), content, svgGraph => {
element.innerHTML = svgGraph
if (!isPredefined) {
const el = element.firstChild
const viewBox = el.getAttribute('viewBox').split(' ')
let ratio = viewBox[2] / viewBox[3]
if (el.style.maxWidth) {
const maxWidth = parseFloat(el.style.maxWidth)
ratio *= el.parentNode.clientWidth / maxWidth
}
el.setAttribute('ratio', ratio)
el.setAttribute('height', el.parentNode.clientWidth / ratio)
}
})
} catch (e) {
element.className = 'mermaid-error'
element.innerHTML = 'mermaid diagram parse error: ' + e.message
}
}
export default render

View File

@@ -1,78 +0,0 @@
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'
}

View File

@@ -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, { handleNotFoundStyleName: 'log' }) return CSSModules(component, styles, {errorWhenNotFound: false})
} }

View File

@@ -1,90 +0,0 @@
const languages = [
{
name: 'Albanian',
locale: 'sq'
},
{
name: 'Chinese (zh-CN)',
locale: 'zh-CN'
},
{
name: 'Chinese (zh-TW)',
locale: 'zh-TW'
},
{
name: 'Czech',
locale: 'cs'
},
{
name: 'Danish',
locale: 'da'
},
{
name: 'English',
locale: 'en'
},
{
name: 'French',
locale: 'fr'
},
{
name: 'German',
locale: 'de'
},
{
name: 'Hungarian',
locale: 'hu'
},
{
name: 'Japanese',
locale: 'ja'
},
{
name: 'Korean',
locale: 'ko'
},
{
name: 'Norwegian',
locale: 'no'
},
{
name: 'Polish',
locale: 'pl'
},
{
name: 'Portuguese (PT-BR)',
locale: 'pt-BR'
},
{
name: 'Portuguese (PT-PT)',
locale: 'pt-PT'
},
{
name: 'Russian',
locale: 'ru'
},
{
name: 'Spanish',
locale: 'es-ES'
},
{
name: 'Turkish',
locale: 'tr'
},
{
name: 'Thai',
locale: 'th'
}
]
module.exports = {
getLocales() {
return languages.reduce(function(localeList, locale) {
localeList.push(locale.locale)
return localeList
}, [])
},
getLanguages() {
return languages
}
}

View File

@@ -1,43 +1,43 @@
class MutableMap { class MutableMap {
constructor(iterable) { constructor (iterable) {
this._map = new Map(iterable) this._map = new Map(iterable)
Object.defineProperty(this, 'size', { Object.defineProperty(this, 'size', {
get: () => this._map.size, get: () => this._map.size,
set: function(value) { set: function (value) {
this['size'] = value this['size'] = value
} }
}) })
} }
get(...args) { get (...args) {
return this._map.get(...args) return this._map.get(...args)
} }
set(...args) { set (...args) {
return this._map.set(...args) return this._map.set(...args)
} }
delete(...args) { delete (...args) {
return this._map.delete(...args) return this._map.delete(...args)
} }
has(...args) { has (...args) {
return this._map.has(...args) return this._map.has(...args)
} }
clear(...args) { clear (...args) {
return this._map.clear(...args) return this._map.clear(...args)
} }
forEach(...args) { forEach (...args) {
return this._map.forEach(...args) return this._map.forEach(...args)
} }
[Symbol.iterator]() { [Symbol.iterator] () {
return this._map[Symbol.iterator]() return this._map[Symbol.iterator]()
} }
map(cb) { map (cb) {
const result = [] const result = []
for (const [key, value] of this._map) { for (const [key, value] of this._map) {
result.push(cb(value, key)) result.push(cb(value, key))
@@ -45,7 +45,7 @@ class MutableMap {
return result return result
} }
toJS() { toJS () {
const result = {} const result = {}
for (let [key, value] of this._map) { for (let [key, value] of this._map) {
if (value instanceof MutableSet || value instanceof MutableMap) { if (value instanceof MutableSet || value instanceof MutableMap) {
@@ -58,42 +58,42 @@ class MutableMap {
} }
class MutableSet { class MutableSet {
constructor(iterable) { constructor (iterable) {
this._set = new Set(iterable) this._set = new Set(iterable)
Object.defineProperty(this, 'size', { Object.defineProperty(this, 'size', {
get: () => this._set.size, get: () => this._set.size,
set: function(value) { set: function (value) {
this['size'] = value this['size'] = value
} }
}) })
} }
add(...args) { add (...args) {
return this._set.add(...args) return this._set.add(...args)
} }
delete(...args) { delete (...args) {
return this._set.delete(...args) return this._set.delete(...args)
} }
forEach(...args) { forEach (...args) {
return this._set.forEach(...args) return this._set.forEach(...args)
} }
[Symbol.iterator]() { [Symbol.iterator] () {
return this._set[Symbol.iterator]() return this._set[Symbol.iterator]()
} }
map(cb) { map (cb) {
const result = [] const result = []
this._set.forEach(function(value, key) { this._set.forEach(function (value, key) {
result.push(cb(value, key)) result.push(cb(value, key))
}) })
return result return result
} }
toJS() { toJS () {
return Array.from(this._set) return Array.from(this._set)
} }
} }

View File

@@ -5,13 +5,13 @@ const BOOSTNOTERC = '.boostnoterc'
const homePath = global.process.env.HOME || global.process.env.USERPROFILE const homePath = global.process.env.HOME || global.process.env.USERPROFILE
const _boostnotercPath = path.join(homePath, BOOSTNOTERC) const _boostnotercPath = path.join(homePath, BOOSTNOTERC)
export function parse(boostnotercPath = _boostnotercPath) { export function parse (boostnotercPath = _boostnotercPath) {
if (!sander.existsSync(boostnotercPath)) return {} if (!sander.existsSync(boostnotercPath)) return {}
try { try {
return JSON.parse(sander.readFileSync(boostnotercPath).toString()) return JSON.parse(sander.readFileSync(boostnotercPath).toString())
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
console.warn("Your .boostnoterc is broken so it's not used.") console.warn('Your .boostnoterc is broken so it\'s not used.')
return {} return {}
} }
} }

View File

@@ -1,92 +0,0 @@
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

View File

@@ -1,109 +0,0 @@
import { Point } from '@susisu/mte-kernel'
export default class TextEditorInterface {
constructor(editor) {
this.editor = editor
this.doc = editor.getDoc()
this.transaction = false
}
getCursorPosition() {
const { line, ch } = this.doc.getCursor()
return new Point(line, ch)
}
setCursorPosition(pos) {
this.doc.setCursor({
line: pos.row,
ch: pos.column
})
}
setSelectionRange(range) {
this.doc.setSelection(
{ line: range.start.row, ch: range.start.column },
{ line: range.end.row, ch: range.end.column }
)
}
getLastRow() {
return this.doc.lineCount() - 1
}
acceptsTableEdit() {
return true
}
getLine(row) {
return this.doc.getLine(row)
}
insertLine(row, line) {
const lastRow = this.getLastRow()
if (row > lastRow) {
const lastLine = this.getLine(lastRow)
this.doc.replaceRange(
'\n' + line,
{ line: lastRow, ch: lastLine.length },
{ line: lastRow, ch: lastLine.length }
)
} else {
this.doc.replaceRange(
line + '\n',
{ line: row, ch: 0 },
{ line: row, ch: 0 }
)
}
}
deleteLine(row) {
const lastRow = this.getLastRow()
if (row >= lastRow) {
if (lastRow > 0) {
const preLastLine = this.getLine(lastRow - 1)
const lastLine = this.getLine(lastRow)
this.doc.replaceRange(
'',
{ line: lastRow - 1, ch: preLastLine.length },
{ line: lastRow, ch: lastLine.length }
)
} else {
const lastLine = this.getLine(lastRow)
this.doc.replaceRange(
'',
{ line: lastRow, ch: 0 },
{ line: lastRow, ch: lastLine.length }
)
}
} else {
this.doc.replaceRange('', { line: row, ch: 0 }, { line: row + 1, ch: 0 })
}
}
replaceLines(startRow, endRow, lines) {
const lastRow = this.getLastRow()
if (endRow > lastRow) {
const lastLine = this.getLine(lastRow)
this.doc.replaceRange(
lines.join('\n'),
{ line: startRow, ch: 0 },
{ line: lastRow, ch: lastLine.length }
)
} else {
this.doc.replaceRange(
lines.join('\n') + '\n',
{ line: startRow, ch: 0 },
{ line: endRow, ch: 0 }
)
}
}
transact(func) {
this.transaction = true
func()
this.transaction = false
if (this.onDidFinishTransaction) {
this.onDidFinishTransaction.call(undefined)
}
}
}

View File

@@ -1,24 +0,0 @@
import electron from 'electron'
import i18n from 'browser/lib/i18n'
const { remote } = electron
const { dialog } = remote
export function confirmDeleteNote(confirmDeletion, permanent) {
if (confirmDeletion || permanent) {
const alertConfig = {
type: 'warning',
message: i18n.__('Confirm note deletion'),
detail: i18n.__('This will permanently remove this note.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}
const dialogButtonIndex = dialog.showMessageBox(
remote.getCurrentWindow(),
alertConfig
)
return dialogButtonIndex === 0
}
return true
}

View File

@@ -3,59 +3,14 @@ const fs = require('sander')
const { remote } = require('electron') const { remote } = require('electron')
const { app } = remote const { app } = remote
const CODEMIRROR_THEME_PATH = 'node_modules/codemirror/theme' const themePath = process.env.NODE_ENV === 'production'
const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme' ? path.join(app.getAppPath(), './node_modules/codemirror/theme')
: require('path').resolve('./node_modules/codemirror/theme')
const isProduction = process.env.NODE_ENV === 'production' const themes = fs.readdirSync(themePath)
.map((themePath) => {
const paths = [ return themePath.substring(0, themePath.lastIndexOf('.'))
isProduction })
? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
: path.resolve(CODEMIRROR_THEME_PATH),
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'
? path.join(app.getPath('userData'), 'snippets.json')
: '' // return nothing as we specified different path to snippets.json in test
const consts = { const consts = {
FOLDER_COLORS: [ FOLDER_COLORS: [
@@ -76,16 +31,7 @@ const consts = {
'Dodger Blue', 'Dodger Blue',
'Violet Eggplant' 'Violet Eggplant'
], ],
THEMES: themes, THEMES: ['default'].concat(themes)
SNIPPET_FILE: snippetFile,
DEFAULT_EDITOR_FONT_FAMILY: [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
} }
module.exports = consts module.exports = consts

View File

@@ -1,9 +1,9 @@
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, MenuItem } = remote const { Menu, MenuItem } = remote
function popup(templates) { function popup (templates) {
const menu = new Menu() const menu = new Menu()
templates.forEach(item => { templates.forEach((item) => {
menu.append(new MenuItem(item)) menu.append(new MenuItem(item))
}) })
menu.popup(remote.getCurrentWindow()) menu.popup(remote.getCurrentWindow())

View File

@@ -1,152 +0,0 @@
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
}

View File

@@ -1,14 +0,0 @@
export default function convertModeName(name) {
switch (name) {
case 'ejs':
return 'Embedded Javascript'
case 'html_ruby':
return 'Embedded Ruby'
case 'objectivec':
return 'Objective C'
case 'text':
return 'Plain Text'
default:
return name
}
}

View File

@@ -1,21 +1,5 @@
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir' import 'codemirror-mode-elixir'
const stylusCodeInfo = CodeMirror.modeInfo.find(info => info.name === 'Stylus') CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
if (stylusCodeInfo == null) { CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})
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']
})

View File

@@ -8,7 +8,7 @@ import moment from 'moment'
* @param {mixed} * @param {mixed}
* @return {string} * @return {string}
*/ */
export function formatDate(date) { export function formatDate (date) {
const m = moment(date) const m = moment(date)
if (!m.isValid()) { if (!m.isValid()) {
throw Error('Invalid argument.') throw Error('Invalid argument.')

View File

@@ -1,54 +1,23 @@
export function findNoteTitle( export function findNoteTitle (value) {
value,
enableFrontMatterTitle,
frontMatterTitleField = 'title'
) {
const splitted = value.split('\n') const splitted = value.split('\n')
let title = null let title = null
let isInsideCodeBlock = false let isInsideCodeBlock = false
if (splitted[0] === '---') { splitted.some((line, index) => {
let line = 0 const trimmedLine = line.trim()
while (++line < splitted.length) { const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
if ( if (trimmedLine.match('```')) {
enableFrontMatterTitle && isInsideCodeBlock = !isInsideCodeBlock
splitted[line].startsWith(frontMatterTitleField + ':')
) {
title = splitted[line]
.substring(frontMatterTitleField.length + 1)
.trim()
break
}
if (splitted[line] === '---') {
splitted.splice(0, line + 1)
break
}
} }
} if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
title = trimmedLine
if (title === null) { return true
splitted.some((line, index) => { }
const trimmedLine = line.trim() })
const trimmedNextLine =
splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
if (trimmedLine.match('```')) {
isInsideCodeBlock = !isInsideCodeBlock
}
if (
isInsideCodeBlock === false &&
(trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))
) {
title = trimmedLine
return true
}
})
}
if (title === null) { if (title === null) {
title = '' title = ''
splitted.some(line => { splitted.some((line) => {
if (line.trim().length > 0) { if (line.trim().length > 0) {
title = line.trim() title = line.trim()
return true return true

View File

@@ -1,11 +1,10 @@
const _ = require('lodash') const _ = require('lodash')
export function findStorage(storageKey) { export function findStorage (storageKey) {
const cachedStorageList = JSON.parse(localStorage.getItem('storages')) const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
throw new Error("Target storage doesn't exist.") const storage = _.find(cachedStorageList, {key: storageKey})
const storage = _.find(cachedStorageList, { key: storageKey }) if (storage === undefined) throw new Error('Target storage doesn\'t exist.')
if (storage === undefined) throw new Error("Target storage doesn't exist.")
return storage return storage
} }

View File

@@ -1,14 +1,14 @@
export function getTodoStatus(content) { export function getTodoStatus (content) {
const splitted = content.split('\n') const splitted = content.split('\n')
let numberOfTodo = 0 let numberOfTodo = 0
let numberOfCompletedTodo = 0 let numberOfCompletedTodo = 0
splitted.forEach(line => { splitted.forEach((line) => {
const trimmedLine = line.trim().replace(/^(>\s*)*/, '') const trimmedLine = line.trim()
if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) { if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
numberOfTodo++ numberOfTodo++
} }
if (trimmedLine.match(/^[+\-*] \[x] ./i)) { if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
numberOfCompletedTodo++ numberOfCompletedTodo++
} }
}) })
@@ -19,7 +19,7 @@ export function getTodoStatus(content) {
} }
} }
export function getTodoPercentageOfCompleted(content) { export function getTodoPercentageOfCompleted (content) {
const state = getTodoStatus(content) const state = getTodoStatus(content)
return Math.floor((state.completed / state.total) * 100) return Math.floor(state.completed / state.total * 100)
} }

View File

@@ -7,9 +7,9 @@
* @return {string} * @return {string}
*/ */
export function decodeEntities(text) { export function decodeEntities (text) {
var entities = [ var entities = [
['apos', "'"], ['apos', '\''],
['amp', '&'], ['amp', '&'],
['lt', '<'], ['lt', '<'],
['gt', '>'], ['gt', '>'],
@@ -24,16 +24,16 @@ export function decodeEntities(text) {
return text return text
} }
export function encodeEntities(text) { export function encodeEntities (text) {
const entities = [ const entities = [
["'", 'apos'], ['\'', 'apos'],
['<', 'lt'], ['<', 'lt'],
['>', 'gt'], ['>', 'gt'],
['\\?', '#63'], ['\\?', '#63'],
['\\$', '#36'] ['\\$', '#36']
] ]
entities.forEach(entity => { entities.forEach((entity) => {
text = text.replace(new RegExp(entity[0], 'g'), `&${entity[1]};`) text = text.replace(new RegExp(entity[0], 'g'), `&${entity[1]};`)
}) })
return text return text

View File

@@ -1,18 +0,0 @@
const path = require('path')
const { remote } = require('electron')
const { app } = remote
const { getLocales } = require('./Languages.js')
// load package for localization
const i18n = new (require('i18n-2'))({
// setup some locales - other locales default to the first locale
locales: getLocales(),
extension: '.json',
directory:
process.env.NODE_ENV === 'production'
? path.join(app.getAppPath(), './locales')
: path.resolve('./locales'),
devMode: false
})
export default i18n

View File

@@ -1,10 +1,7 @@
const crypto = require('crypto') const crypto = require('crypto')
const uuidv4 = require('uuid/v4') const _ = require('lodash')
module.exports = function(uuid) { module.exports = function (length) {
if (typeof uuid === typeof true && uuid) { if (!_.isFinite(length)) length = 10
return uuidv4()
}
const length = 10
return crypto.randomBytes(length).toString('hex') return crypto.randomBytes(length).toString('hex')
} }

View File

@@ -1,282 +0,0 @@
'use strict'
module.exports = function definitionListPlugin(md) {
var isSpace = md.utils.isSpace
// Search `[:~][\n ]`, returns next pos after marker on success
// or -1 on fail.
function skipMarker(state, line) {
let start = state.bMarks[line] + state.tShift[line]
const max = state.eMarks[line]
if (start >= max) {
return -1
}
// Check bullet
const marker = state.src.charCodeAt(start++)
if (marker !== 0x7e /* ~ */ && marker !== 0x3a /* : */) {
return -1
}
const pos = state.skipSpaces(start)
// require space after ":"
if (start === pos) {
return -1
}
return start
}
function markTightParagraphs(state, idx) {
const level = state.level + 2
let i
let l
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
if (
state.tokens[i].level === level &&
state.tokens[i].type === 'paragraph_open'
) {
state.tokens[i + 2].hidden = true
state.tokens[i].hidden = true
i += 2
}
}
}
function deflist(state, startLine, endLine, silent) {
var ch,
contentStart,
ddLine,
dtLine,
itemLines,
listLines,
listTokIdx,
max,
newEndLine,
nextLine,
offset,
oldDDIndent,
oldIndent,
oldLineMax,
oldParentType,
oldSCount,
oldTShift,
oldTight,
pos,
prevEmptyEnd,
tight,
token
if (silent) {
// quirk: validation mode validates a dd block only, not a whole deflist
if (state.ddIndent < 0) {
return false
}
return skipMarker(state, startLine) >= 0
}
nextLine = startLine + 1
if (nextLine >= endLine) {
return false
}
if (state.isEmpty(nextLine)) {
nextLine++
if (nextLine >= endLine) {
return false
}
}
if (state.sCount[nextLine] < state.blkIndent) {
return false
}
contentStart = skipMarker(state, nextLine)
if (contentStart < 0) {
return false
}
// Start list
listTokIdx = state.tokens.length
tight = true
token = state.push('dl_open', 'dl', 1)
token.map = listLines = [startLine, 0]
//
// Iterate list items
//
dtLine = startLine
ddLine = nextLine
// One definition list can contain multiple DTs,
// and one DT can be followed by multiple DDs.
//
// Thus, there is two loops here, and label is
// needed to break out of the second one
//
/* eslint no-labels:0,block-scoped-var:0 */
OUTER: for (;;) {
prevEmptyEnd = false
token = state.push('dt_open', 'dt', 1)
token.map = [dtLine, dtLine]
token = state.push('inline', '', 0)
token.map = [dtLine, dtLine]
token.content = state
.getLines(dtLine, dtLine + 1, state.blkIndent, false)
.trim()
token.children = []
token = state.push('dt_close', 'dt', -1)
for (;;) {
token = state.push('dd_open', 'dd', 1)
token.map = itemLines = [ddLine, 0]
pos = contentStart
max = state.eMarks[ddLine]
offset =
state.sCount[ddLine] +
contentStart -
(state.bMarks[ddLine] + state.tShift[ddLine])
while (pos < max) {
ch = state.src.charCodeAt(pos)
if (isSpace(ch)) {
if (ch === 0x09) {
offset += 4 - (offset % 4)
} else {
offset++
}
} else {
break
}
pos++
}
contentStart = pos
oldTight = state.tight
oldDDIndent = state.ddIndent
oldIndent = state.blkIndent
oldTShift = state.tShift[ddLine]
oldSCount = state.sCount[ddLine]
oldParentType = state.parentType
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
state.sCount[ddLine] = offset
state.tight = true
state.parentType = 'deflist'
newEndLine = ddLine
while (
++newEndLine < endLine &&
(state.sCount[newEndLine] >= state.sCount[ddLine] ||
state.isEmpty(newEndLine))
) {}
oldLineMax = state.lineMax
state.lineMax = newEndLine
state.md.block.tokenize(state, ddLine, newEndLine, true)
state.lineMax = oldLineMax
// If any of list item is tight, mark list as tight
if (!state.tight || prevEmptyEnd) {
tight = false
}
// Item become loose if finish with empty line,
// but we should filter last element, because it means list finish
prevEmptyEnd = state.line - ddLine > 1 && state.isEmpty(state.line - 1)
state.tShift[ddLine] = oldTShift
state.sCount[ddLine] = oldSCount
state.tight = oldTight
state.parentType = oldParentType
state.blkIndent = oldIndent
state.ddIndent = oldDDIndent
token = state.push('dd_close', 'dd', -1)
itemLines[1] = nextLine = state.line
if (nextLine >= endLine) {
break OUTER
}
if (state.sCount[nextLine] < state.blkIndent) {
break OUTER
}
contentStart = skipMarker(state, nextLine)
if (contentStart < 0) {
break
}
ddLine = nextLine
// go to the next loop iteration:
// insert DD tag and repeat checking
}
if (nextLine >= endLine) {
break
}
dtLine = nextLine
if (state.isEmpty(dtLine)) {
break
}
if (state.sCount[dtLine] < state.blkIndent) {
break
}
ddLine = dtLine + 1
if (ddLine >= endLine) {
break
}
if (state.isEmpty(ddLine)) {
ddLine++
}
if (ddLine >= endLine) {
break
}
if (state.sCount[ddLine] < state.blkIndent) {
break
}
contentStart = skipMarker(state, ddLine)
if (contentStart < 0) {
break
}
// go to the next loop iteration:
// insert DT and DD tags and repeat checking
}
// Finilize list
token = state.push('dl_close', 'dl', -1)
listLines[1] = nextLine
state.line = nextLine
// mark paragraphs tight if needed
if (tight) {
markTightParagraphs(state, listTokIdx)
}
return true
}
md.block.ruler.before('paragraph', 'deflist', deflist, {
alt: ['paragraph', 'reference']
})
}

View File

@@ -1,141 +0,0 @@
'use strict'
module.exports = function(md, renderers, defaultRenderer) {
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
function fence(state, startLine, endLine, silent) {
let pos = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine]
if (state.sCount[startLine] - state.blkIndent >= 4 || pos + 3 > max) {
return false
}
const marker = state.src.charCodeAt(pos)
if (marker !== 0x7e /* ~ */ && marker !== 0x60 /* ` */) {
return false
}
let mem = pos
pos = state.skipChars(pos, marker)
let len = pos - mem
if (len < 3) {
return false
}
const markup = state.src.slice(mem, pos)
const params = state.src.slice(pos, max)
if (silent) {
return true
}
let nextLine = startLine
let haveEndMarker = false
while (true) {
nextLine++
if (nextLine >= endLine) {
break
}
pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
max = state.eMarks[nextLine]
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
break
}
if (
state.src.charCodeAt(pos) !== marker ||
state.sCount[nextLine] - state.blkIndent >= 4
) {
continue
}
pos = state.skipChars(pos, marker)
if (pos - mem < len) {
continue
}
pos = state.skipSpaces(pos)
if (pos >= max) {
haveEndMarker = true
break
}
}
len = state.sCount[startLine]
state.line = nextLine + (haveEndMarker ? 1 : 0)
const parameters = {}
let langType = ''
let fileName = ''
let firstLineNumber = 1
let match = paramsRE.exec(params)
if (match) {
if (match[1]) {
langType = match[1]
}
if (match[3]) {
fileName = match[3]
}
if (match[4]) {
firstLineNumber = parseInt(match[4], 10)
}
if (match[2]) {
const params = match[2]
const regex = /(\w[-\w]*)(?:=(?:'(.*?[^\\])?'|"(.*?[^\\])?"|([^'"][^\s]*)))?/g
let name, value
while ((match = regex.exec(params))) {
name = match[1]
value = match[2] || match[3] || match[4] || null
const height = /^(\d+)h$/.exec(name)
if (height && !value) {
parameters.height = height[1]
} else {
parameters[name] = value
}
}
}
}
let token
if (renderers[langType]) {
token = state.push(`${langType}_fence`, 'div', 0)
} else {
token = state.push('_fence', 'code', 0)
}
token.langType = langType
token.fileName = fileName
token.firstLineNumber = firstLineNumber
token.parameters = parameters
token.content = state.getLines(startLine + 1, nextLine, len, true)
token.markup = markup
token.map = [startLine, state.line]
return true
}
md.block.ruler.before('fence', '_fence', fence, {
alt: ['paragraph', 'reference', 'blockquote', 'list']
})
for (const name in renderers) {
md.renderer.rules[`${name}_fence`] = (tokens, index) =>
renderers[name](tokens[index])
}
if (defaultRenderer) {
md.renderer.rules['_fence'] = (tokens, index) =>
defaultRenderer(tokens[index])
}
}

View File

@@ -1,29 +0,0 @@
'use strict'
module.exports = function frontMatterPlugin(md) {
function frontmatter(state, startLine, endLine, silent) {
if (
startLine !== 0 ||
state.src.substr(startLine, state.eMarks[0]) !== '---'
) {
return false
}
let line = 0
while (++line < state.lineMax) {
if (
state.src.substring(state.bMarks[line], state.eMarks[line]) === '---'
) {
state.line = line + 1
return true
}
}
return false
}
md.block.ruler.before('table', 'frontmatter', frontmatter, {
alt: ['paragraph', 'reference', 'blockquote', 'list']
})
}

View File

@@ -1,141 +0,0 @@
'use strict'
import sanitizeHtml from 'sanitize-html'
import { escapeHtmlCharacters } from './utils'
import url from 'url'
module.exports = function sanitizePlugin(md, options) {
options = options || {}
md.core.ruler.after('linkify', 'sanitize_inline', state => {
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
if (state.tokens[tokenIdx].type === 'html_block') {
state.tokens[tokenIdx].content = sanitizeHtml(
state.tokens[tokenIdx].content,
options
)
}
if (state.tokens[tokenIdx].type.match(/.*_fence$/)) {
// escapeHtmlCharacters has better performance
state.tokens[tokenIdx].content = escapeHtmlCharacters(
state.tokens[tokenIdx].content,
{ skipSingleQuote: true }
)
}
if (state.tokens[tokenIdx].type === 'inline') {
const inlineTokens = state.tokens[tokenIdx].children
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
if (inlineTokens[childIdx].type === 'html_inline') {
inlineTokens[childIdx].content = sanitizeInline(
inlineTokens[childIdx].content,
options
)
}
}
}
}
})
}
const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/gi
function sanitizeInline(html, options) {
let match = tagRegex.exec(html)
if (!match) {
return ''
}
const {
allowedTags,
allowedAttributes,
selfClosing,
allowedSchemesAppliedToAttributes
} = options
if (match[1] !== undefined) {
// opening tag
const tag = match[1].toLowerCase()
if (allowedTags.indexOf(tag) === -1) {
return ''
}
const attributes = match[2]
let attrs = ''
let name
let value
while ((match = attributesRegex.exec(attributes))) {
name = match[1].toLowerCase()
value = match[3]
if (
allowedAttributes['*'].indexOf(name) !== -1 ||
(allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)
) {
if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
if (
naughtyHRef(value, options) ||
(tag === 'iframe' &&
name === 'src' &&
naughtyIFrame(value, options))
) {
continue
}
}
attrs += ` ${name}`
if (match[2]) {
attrs += `="${value}"`
}
}
}
if (selfClosing.indexOf(tag) === -1) {
return '<' + tag + attrs + '>'
} else {
return '<' + tag + attrs + ' />'
}
} else {
// closing tag
if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) {
return html
} else {
return ''
}
}
}
function naughtyHRef(href, options) {
// href = href.replace(/[\x00-\x20]+/g, '')
if (!href) {
// No href
return false
}
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
const matches = href.match(/^([a-zA-Z]+)\:/)
if (!matches) {
if (href.match(/^[\/\\]{2}/)) {
return !options.allowProtocolRelative
}
// No scheme
return false
}
const scheme = matches[1].toLowerCase()
return options.allowedSchemes.indexOf(scheme) === -1
}
function naughtyIFrame(src, options) {
try {
const parsed = url.parse(src, false, true)
return options.allowedIframeHostnames.index(parsed.hostname) === -1
} catch (e) {
return true
}
}

View File

@@ -1,104 +0,0 @@
/**
* @fileoverview Markdown table of contents generator
*/
import { EOL } from 'os'
import toc from 'markdown-toc'
import mdlink from 'markdown-link'
import slugify from './slugify'
const hasProp = Object.prototype.hasOwnProperty
/**
* From @enyaxu/markdown-it-anchor
*/
function uniqueSlug(slug, slugs, opts) {
let uniq = slug
let i = opts.uniqueSlugStartIndex
while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
slugs[uniq] = true
return uniq
}
function linkify(token) {
token.content = mdlink(token.content, `#${decodeURI(token.slug)}`)
return token
}
const TOC_MARKER_START = '<!-- toc -->'
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.
* If TOC doesn't exit in the editor, it's inserted at current caret position.
* Otherwise,TOC is updated in place.
* @param editor CodeMirror editor to be updated with TOC
*/
export function generateInEditor(editor) {
function updateExistingToc() {
const toc = generate(editor.getValue())
const search = editor.getSearchCursor(tocRegex)
while (search.findNext()) {
search.replace(toc)
}
}
function addTocAtCursorPosition() {
const toc = generate(
editor.getRange(editor.getCursor(), { line: Infinity })
)
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
}
if (tocExistsInEditor(editor)) {
updateExistingToc()
} else {
addTocAtCursorPosition()
}
}
export function tocExistsInEditor(editor) {
return tocRegex.test(editor.getValue())
}
/**
* Generates MD TOC based on MD document passed as string.
* @param markdownText MD document
* @returns generatedTOC String containing generated TOC
*/
export function generate(markdownText) {
const slugs = {}
const opts = {
uniqueSlugStartIndex: 1
}
const result = toc(markdownText, {
slugify: title => {
return uniqueSlug(slugify(title), slugs, opts)
},
linkify: false
})
const md = toc.bullets(result.json.map(linkify), {
highest: result.highest
})
return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
}
function wrapTocWithEol(toc, editor) {
const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
const rightWrap =
editor.getLine(editor.getCursor().line).length === editor.getCursor().ch
? ''
: EOL
return leftWrap + toc + rightWrap
}
export default {
generate,
generateInEditor,
tocExistsInEditor
}

View File

@@ -1,526 +1,173 @@
import markdownit from 'markdown-it' import markdownit from 'markdown-it'
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 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 { escapeHtmlCharacters, lastFindInArray } from './utils'
function createGutter(str, firstLineNumber) { // FIXME We should not depend on global variable.
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1 const katex = window.katex
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1 const config = ConfigManager.get()
function createGutter (str) {
const lc = (str.match(/\n/g) || []).length
const lines = [] const lines = []
for (let i = firstLineNumber; i <= lastLineNumber; i++) { for (let i = 1; i <= lc; i++) {
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>') lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
} }
return ( return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
'<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
)
} }
class Markdown { var md = markdownit({
constructor(options = {}) { typographer: true,
const config = ConfigManager.get() linkify: true,
const defaultOptions = { html: true,
typographer: config.preview.smartQuotes, xhtmlOut: true,
linkify: true, breaks: true,
html: true, highlight: function (str, lang) {
xhtmlOut: true, if (lang === 'flowchart') {
breaks: config.preview.breaks, return `<pre class="flowchart">${str}</pre>`
sanitize: 'STRICT',
onFence: () => {}
} }
if (lang === 'sequence') {
const updatedOptions = Object.assign(defaultOptions, options) return `<pre class="sequence">${str}</pre>`
this.md = markdownit(updatedOptions)
this.md.linkify.set({ fuzzyLink: false })
if (updatedOptions.sanitize !== 'NONE') {
const allowedTags = [
'iframe',
'input',
'b',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'h7',
'h8',
'br',
'b',
'i',
'strong',
'em',
'a',
'pre',
'code',
'img',
'tt',
'div',
'ins',
'del',
'sup',
'sub',
'p',
'ol',
'ul',
'table',
'thead',
'tbody',
'tfoot',
'blockquote',
'dl',
'dt',
'dd',
'kbd',
'q',
'samp',
'var',
'hr',
'ruby',
'rt',
'rp',
'li',
'tr',
'td',
'th',
's',
'strike',
'summary',
'details'
]
const allowedAttributes = [
'abbr',
'accept',
'accept-charset',
'accesskey',
'action',
'align',
'alt',
'axis',
'border',
'cellpadding',
'cellspacing',
'char',
'charoff',
'charset',
'checked',
'clear',
'cols',
'colspan',
'color',
'compact',
'coords',
'datetime',
'dir',
'disabled',
'enctype',
'for',
'frame',
'headers',
'height',
'hreflang',
'hspace',
'ismap',
'label',
'lang',
'maxlength',
'media',
'method',
'multiple',
'name',
'nohref',
'noshade',
'nowrap',
'open',
'prompt',
'readonly',
'rel',
'rev',
'rows',
'rowspan',
'rules',
'scope',
'selected',
'shape',
'size',
'span',
'start',
'summary',
'tabindex',
'target',
'title',
'type',
'usemap',
'valign',
'value',
'vspace',
'width',
'itemprop'
]
if (updatedOptions.sanitize === 'ALLOW_STYLES') {
allowedTags.push('style')
allowedAttributes.push('style')
}
// Sanitize use rinput before other plugins
this.md.use(sanitize, {
allowedTags,
allowedAttributes: {
'*': allowedAttributes,
a: ['href'],
div: ['itemscope', 'itemtype'],
blockquote: ['cite'],
del: ['cite'],
ins: ['cite'],
q: ['cite'],
img: ['src', 'width', 'height'],
iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
input: ['type', 'id', 'checked']
},
allowedIframeHostnames: ['www.youtube.com'],
selfClosing: ['img', 'br', 'hr', 'input'],
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
allowProtocolRelative: true
})
} }
return '<pre class="code">' +
createGutter(str) +
'<code class="' + lang + '">' +
str +
'</code></pre>'
}
})
md.use(emoji, {
shortcuts: {}
})
md.use(math, {
inlineOpen: config.preview.latexInlineOpen,
inlineClose: config.preview.latexInlineClose,
blockOpen: config.preview.latexBlockOpen,
blockClose: config.preview.latexBlockClose,
inlineRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim(), { displayMode: true })
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
md.use(require('markdown-it-imsize'))
md.use(require('markdown-it-footnote'))
md.use(require('markdown-it-multimd-table'))
md.use(require('markdown-it-named-headers'), {
slugify: (header) => {
return encodeURI(header.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
}
})
md.use(require('markdown-it-kbd'))
this.md.use(emoji, { const deflate = require('markdown-it-plantuml/lib/deflate')
shortcuts: {} md.use(require('markdown-it-plantuml'), '', {
}) generateSource: function (umlCode) {
this.md.use(math, { const s = unescape(encodeURIComponent(umlCode))
inlineOpen: config.preview.latexInlineOpen, const zippedCode = deflate.encode64(
inlineClose: config.preview.latexInlineClose, deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
blockOpen: config.preview.latexBlockOpen,
blockClose: config.preview.latexBlockClose,
inlineRenderer: function(str) {
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function(str) {
let output = ''
try {
output = katex.renderToString(str.trim(), { displayMode: true })
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
this.md.use(require('markdown-it-imsize'))
this.md.use(require('markdown-it-footnote'))
this.md.use(require('markdown-it-multimd-table'))
this.md.use(require('@enyaxu/markdown-it-anchor'), {
slugify: require('./slugify')
})
this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'), {
types: [
'note',
'hint',
'attention',
'caution',
'danger',
'error',
'quote',
'abstract',
'question'
]
})
this.md.use(require('markdown-it-abbr'))
this.md.use(require('markdown-it-sub'))
this.md.use(require('markdown-it-sup'))
this.md.use(md => {
markdownItTocAndAnchor(md, {
toc: true,
tocPattern: /\[TOC\]/i,
anchorLink: false,
appendIdToHeading: false
})
md.renderer.rules.toc_open = () => '<div class="markdownIt-TOC-wrapper">'
md.renderer.rules.toc_close = () => '</div>'
})
this.md.use(require('./markdown-it-deflist'))
this.md.use(require('./markdown-it-frontmatter'))
this.md.use(
require('./markdown-it-fence'),
{
chart: token => {
if (token.parameters.hasOwnProperty('yaml')) {
token.parameters.format = 'yaml'
}
updatedOptions.onFence('chart', token.parameters.format)
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="chart" data-height="${
token.parameters.height
}" data-format="${token.parameters.format || 'json'}">${
token.content
}</div>
</pre>`
},
flowchart: token => {
updatedOptions.onFence('flowchart')
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="flowchart" data-height="${token.parameters.height}">${
token.content
}</div>
</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 => {
updatedOptions.onFence('mermaid')
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="mermaid" data-height="${token.parameters.height}">${
token.content
}</div>
</pre>`
},
sequence: token => {
updatedOptions.onFence('sequence')
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="sequence" data-height="${token.parameters.height}">${
token.content
}</div>
</pre>`
}
},
token => {
updatedOptions.onFence('code', token.langType)
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
${createGutter(token.content, token.firstLineNumber)}
<code class="${token.langType}">${token.content}</code>
</pre>`
}
) )
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
}
})
const deflate = require('markdown-it-plantuml/lib/deflate') // Override task item
const plantuml = require('markdown-it-plantuml') md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
const plantUmlStripTrailingSlash = url => let content, terminate, i, l, token
url.endsWith('/') ? url.slice(0, -1) : url let nextLine = startLine + 1
const plantUmlServerAddress = plantUmlStripTrailingSlash( const terminatorRules = state.md.block.ruler.getRules('paragraph')
config.preview.plantUMLServerAddress const endLine = state.lineMax
)
const parsePlantUml = function(umlCode, openMarker, closeMarker, type) {
const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64(
deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9)
)
return `${plantUmlServerAddress}/${type}/${zippedCode}`
}
this.md.use(plantuml, { // jump line-by-line until empty one or EOF
generateSource: umlCode => for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
parsePlantUml(umlCode, '@startuml', '@enduml', 'svg') // this would be a code block normally, but after paragraph
}) // it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
// Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment. // quirk for blockquotes, this line should already be checked by that rule
this.md.use(plantuml, { if (state.sCount[nextLine] < 0) { continue }
openMarker: '@startditaa',
closeMarker: '@endditaa',
generateSource: umlCode =>
parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
})
// Mindmap support // Some tags can terminate paragraph without empty line.
this.md.use(plantuml, { terminate = false
openMarker: '@startmindmap', for (i = 0, l = terminatorRules.length; i < l; i++) {
closeMarker: '@endmindmap', if (terminatorRules[i](state, nextLine, endLine, true)) {
generateSource: umlCode => terminate = true
parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg') break
})
// 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
this.md.block.ruler.at('paragraph', function(
state,
startLine /*, endLine */
) {
let content, terminate, i, l, token
let nextLine = startLine + 1
const terminatorRules = state.md.block.ruler.getRules('paragraph')
const endLine = state.lineMax
// jump line-by-line until empty one or EOF
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
// this would be a code block normally, but after paragraph
// it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) {
continue
}
// quirk for blockquotes, this line should already be checked by that rule
if (state.sCount[nextLine] < 0) {
continue
}
// Some tags can terminate paragraph without empty line.
terminate = false
for (i = 0, l = terminatorRules.length; i < l; i++) {
if (terminatorRules[i](state, nextLine, endLine, true)) {
terminate = true
break
}
}
if (terminate) {
break
}
} }
content = state
.getLines(startLine, nextLine, state.blkIndent, false)
.trim()
state.line = nextLine
token = state.push('paragraph_open', 'p', 1)
token.map = [startLine, state.line]
if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) {
const liToken = lastFindInArray(
state.tokens,
token => token.type === 'list_item_open'
)
if (liToken) {
if (!liToken.attrs) {
liToken.attrs = []
}
if (config.preview.lineThroughCheckbox) {
liToken.attrs.push([
'class',
`taskListItem${match[1] !== ' ' ? ' checked' : ''}`
])
} else {
liToken.attrs.push(['class', 'taskListItem'])
}
}
content = `<label class='taskListItem${
match[1] !== ' ' ? ' checked' : ''
}' for='checkbox-${startLine + 1}'><input type='checkbox'${
match[1] !== ' ' ? ' checked' : ''
} id='checkbox-${startLine + 1}'/> ${content.substring(
4,
content.length
)}</label>`
}
}
token = state.push('inline', '', 0)
token.content = content
token.map = [startLine, state.line]
token.children = []
token = state.push('paragraph_close', 'p', -1)
return true
})
this.md.renderer.rules.code_inline = function(tokens, idx) {
const token = tokens[idx]
return (
'<code class="inline">' +
escapeHtmlCharacters(token.content) +
'</code>'
)
} }
if (terminate) { break }
if (config.preview.smartArrows) {
this.md.use(smartArrows)
}
// Add line number attribute for scrolling
const originalRender = this.md.renderer.render
this.md.renderer.render = (tokens, options, env) => {
tokens.forEach(token => {
switch (token.type) {
case 'blockquote_open':
case 'dd_open':
case 'dt_open':
case 'heading_open':
case 'list_item_open':
case 'paragraph_open':
case 'table_open':
if (token.map) {
token.attrPush(['data-line', token.map[0]])
}
}
})
const result = originalRender.call(this.md.renderer, tokens, options, env)
return result
}
// FIXME We should not depend on global variable.
window.md = this.md
} }
render(content) { content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
state.line = nextLine
token = state.push('paragraph_open', 'p', 1)
token.map = [startLine, state.line]
if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) {
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
}
}
token = state.push('inline', '', 0)
token.content = content
token.map = [startLine, state.line]
token.children = []
token = state.push('paragraph_close', 'p', -1)
return true
})
// Add line number attribute for scrolling
const originalRender = md.renderer.render
md.renderer.render = function render (tokens, options, env) {
tokens.forEach((token) => {
switch (token.type) {
case 'heading_open':
case 'paragraph_open':
case 'blockquote_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
})
const result = originalRender.call(md.renderer, tokens, options, env)
return result
}
// FIXME We should not depend on global variable.
window.md = md
function normalizeLinkText (linkText) {
return md.normalizeLinkText(linkText)
}
const markdown = {
render: function markdown (content) {
if (!_.isString(content)) content = '' if (!_.isString(content)) content = ''
return this.md.render(content) const renderedContent = md.render(content)
} return renderedContent
},
normalizeLinkText
} }
export default Markdown export default markdown

View File

@@ -6,7 +6,7 @@
* @param {string} input * @param {string} input
* @return {string} * @return {string}
*/ */
export function strip(input) { export function strip (input) {
let output = input let output = input
try { try {
output = output output = output
@@ -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*/gm, '') .replace(/^#{1,6}\s*([^#]*)\s*(#{1,6})?/gm, '$1')
.replace(/(`{3,})(.*?)\1/gm, '$2') .replace(/(`{3,})(.*?)\1/gm, '$2')
.replace(/^-{3,}\s*$/g, '') .replace(/^-{3,}\s*$/g, '')
.replace(/`(.+?)`/g, '$1') .replace(/`(.+?)`/g, '$1')

View File

@@ -1,108 +0,0 @@
import dataApi from 'browser/main/lib/dataApi'
import ee from 'browser/main/lib/eventEmitter'
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,
params,
config
) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let tags = []
if (
config.ui.tagNewNoteWithFilteringTags &&
location.pathname.match(/\/tags/)
) {
tags = params.tagname.split(' ')
}
return dataApi
.createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
tags,
content: '',
linesHighlighted: []
})
.then(note => {
const noteHash = note.key
dispatch({
type: 'UPDATE_NOTE',
note: note
})
dispatch(
push({
pathname: location.pathname,
search: queryString.stringify({ key: noteHash })
})
)
ee.emit('list:jump', noteHash)
ee.emit('detail:focus')
})
}
export function createSnippetNote(
storage,
folder,
dispatch,
location,
params,
config
) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let tags = []
if (
config.ui.tagNewNoteWithFilteringTags &&
location.pathname.match(/\/tags/)
) {
tags = params.tagname.split(' ')
}
const defaultLanguage =
config.editor.snippetDefaultLanguage === 'Auto Detect'
? null
: config.editor.snippetDefaultLanguage
return dataApi
.createNote(storage, {
type: 'SNIPPET_NOTE',
folder: folder,
title: '',
tags,
description: '',
snippets: [
{
name: '',
mode: defaultLanguage,
content: '',
linesHighlighted: []
}
]
})
.then(note => {
const noteHash = note.key
dispatch({
type: 'UPDATE_NOTE',
note: note
})
dispatch(
push({
pathname: location.pathname,
search: queryString.stringify({ key: noteHash })
})
)
ee.emit('list:jump', noteHash)
ee.emit('detail:focus')
})
}

View File

@@ -1,9 +0,0 @@
import consts from 'browser/lib/consts'
import isString from 'lodash/isString'
export default function normalizeEditorFontFamily(fontFamily) {
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
return isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily).join(', ')
: defaultEditorFontFamily.join(', ')
}

View File

@@ -1,40 +1,42 @@
import _ from 'lodash' import _ from 'lodash'
export default function searchFromNotes(notes, search) { export default function searchFromNotes (notes, search) {
if (search.trim().length === 0) return [] if (search.trim().length === 0) return []
const searchBlocks = search.split(' ').filter(block => { const searchBlocks = search.split(' ').filter(block => { return block !== '' })
return block !== ''
})
let foundNotes = notes let foundNotes = findByWord(notes, searchBlocks[0])
searchBlocks.forEach(block => { searchBlocks.forEach((block) => {
foundNotes = findByWordOrTag(foundNotes, block) foundNotes = findByWord(foundNotes, block)
if (block.match(/^#.+/)) {
foundNotes = foundNotes.concat(findByTag(notes, block))
}
}) })
return foundNotes return foundNotes
} }
function findByWordOrTag(notes, block) { function findByTag (notes, block) {
let tag = block const tag = block.match(/#(.+)/)[1]
if (tag.match(/^#.+/)) { const regExp = new RegExp(_.escapeRegExp(tag), 'i')
tag = tag.match(/#(.+)/)[1] return notes.filter((note) => {
} if (!_.isArray(note.tags)) return false
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i') return note.tags.some((_tag) => {
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i') return _tag.match(regExp)
return notes.filter(note => { })
if (_.isArray(note.tags) && note.tags.some(_tag => _tag.match(tagRegExp))) { })
}
function findByWord (notes, block) {
const regExp = new RegExp(_.escapeRegExp(block), 'i')
return notes.filter((note) => {
if (_.isArray(note.tags) && note.tags.some((_tag) => {
return _tag.match(regExp)
})) {
return true return true
} }
if (note.type === 'SNIPPET_NOTE') { if (note.type === 'SNIPPET_NOTE') {
return ( return note.description.match(regExp)
note.description.match(wordRegExp) ||
note.snippets.some(snippet => {
return (
snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
)
})
)
} else if (note.type === 'MARKDOWN_NOTE') { } else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(wordRegExp) return note.content.match(regExp)
} }
return false return false
}) })

View File

@@ -1,15 +0,0 @@
module.exports = function slugify(title) {
const slug = encodeURI(
title
.trim()
.replace(/^\s+/, '')
.replace(/\s+$/, '')
.replace(/\s+/g, '-')
.replace(
/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g,
''
)
)
return slug
}

View File

@@ -1,255 +0,0 @@
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
}

View File

@@ -1,9 +0,0 @@
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
}

View File

@@ -1,44 +0,0 @@
import i18n from 'browser/lib/i18n'
export default [
{
name: 'dark',
label: i18n.__('Dark'),
isDark: true
},
{
name: 'default',
label: i18n.__('Default'),
isDark: false
},
{
name: 'dracula',
label: i18n.__('Dracula'),
isDark: true
},
{
name: 'monokai',
label: i18n.__('Monokai'),
isDark: true
},
{
name: 'nord',
label: i18n.__('Nord'),
isDark: true
},
{
name: 'solarized-dark',
label: i18n.__('Solarized Dark'),
isDark: true
},
{
name: 'vulcan',
label: i18n.__('Vulcan'),
isDark: true
},
{
name: 'white',
label: i18n.__('White'),
isDark: false
}
]

View File

@@ -1,161 +0,0 @@
export function lastFindInArray(array, callback) {
for (let i = array.length - 1; i >= 0; --i) {
if (callback(array[i], i, array)) {
return array[i]
}
}
}
export function escapeHtmlCharacters(
html,
opt = { detectCodeBlock: false, skipSingleQuote: false }
) {
const matchHtmlRegExp = /["'&<>]/g
const matchCodeBlockRegExp = /```/g
const escapes = ['&quot;', '&amp;', '&#39;', '&lt;', '&gt;']
let match = null
const replaceAt = (str, index, replace) =>
str.substr(0, index) +
replace +
str.substr(index + replace.length - (replace.length - 1))
while ((match = matchHtmlRegExp.exec(html)) !== null) {
const current = { char: match[0], index: match.index }
const codeBlockIndexs = []
let openCodeBlock = null
// if the detectCodeBlock option is activated then this function should skip
// characters that needed to be escape but located in code block
if (opt.detectCodeBlock) {
// The first type of code block is lines that start with 4 spaces
// Here we check for the \n character located before the character that
// needed to be escape. It means we check for the begining of the line that
// contain that character, then we check if there are 4 spaces next to the
// \n character (the line start with 4 spaces)
let previousLineEnd = current.index - 1
while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
previousLineEnd--
}
// 4 spaces means this character is in a code block
if (
html[previousLineEnd + 1] === ' ' &&
html[previousLineEnd + 2] === ' ' &&
html[previousLineEnd + 3] === ' ' &&
html[previousLineEnd + 4] === ' '
) {
// skip the current character
continue
}
// The second type of code block is lines that wrapped in ```
// We will get the position of each ```
// then push it into an array
// then the array returned will be like this:
// [startCodeblock, endCodeBlock, startCodeBlock, endCodeBlock]
while ((openCodeBlock = matchCodeBlockRegExp.exec(html)) !== null) {
codeBlockIndexs.push(openCodeBlock.index)
}
let shouldSkipChar = false
// we loop through the array of positions
// we skip 2 element as the i index position is the position of ``` that
// open the codeblock and the i + 1 is the position of the ``` that close
// the code block
for (let i = 0; i < codeBlockIndexs.length; i += 2) {
// the i index position is the position of the ``` that open code block
// so we have to + 2 as that position is the position of the first ` in the ````
// but we need to make sure that the position current character is larger
// that the last ` in the ``` that open the code block so we have to take
// the position of the first ` and + 2
// the i + 1 index position is the closing ``` so the char must less than it
if (
current.index > codeBlockIndexs[i] + 2 &&
current.index < codeBlockIndexs[i + 1]
) {
// skip it
shouldSkipChar = true
break
}
}
if (shouldSkipChar) {
// skip the current character
continue
}
}
// otherwise, escape it !!!
if (current.char === '&') {
// when escaping character & we have to be becareful as the & could be a part
// of an escaped character like &quot; will be came &amp;quot;
let nextStr = ''
let nextIndex = current.index
let escapedStr = false
// maximum length of an escaped string is 5. For example ('&quot;')
// we take the next 5 character of the next string if it is one of the string:
// ['&quot;', '&amp;', '&#39;', '&lt;', '&gt;'] then we will not escape the & character
// as it is a part of the escaped string and should not be escaped
while (nextStr.length <= 5) {
nextStr += html[nextIndex]
nextIndex++
if (escapes.indexOf(nextStr) !== -1) {
escapedStr = true
break
}
}
if (!escapedStr) {
// this & char is not a part of an escaped string
html = replaceAt(html, current.index, '&amp;')
}
} else if (current.char === '"') {
html = replaceAt(html, current.index, '&quot;')
} else if (current.char === "'" && !opt.skipSingleQuote) {
html = replaceAt(html, current.index, '&#39;')
} else if (current.char === '<') {
html = replaceAt(html, current.index, '&lt;')
} else if (current.char === '>') {
html = replaceAt(html, current.index, '&gt;')
}
}
return html
}
export function isObjectEqual(a, b) {
const aProps = Object.getOwnPropertyNames(a)
const bProps = Object.getOwnPropertyNames(b)
if (aProps.length !== bProps.length) {
return false
}
for (var i = 0; i < aProps.length; i++) {
const propName = aProps[i]
if (a[propName] !== b[propName]) {
return false
}
}
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 {
lastFindInArray,
escapeHtmlCharacters,
isObjectEqual,
isMarkdownTitleURL,
humanFileSize
}

View File

@@ -1,49 +0,0 @@
import config from 'browser/main/lib/ConfigManager'
const exec = require('child_process').exec
const path = require('path')
let lastHeartbeat = 0
function sendWakatimeHeartBeat(
storagePath,
noteKey,
storageName,
{ isWrite, hasFileChanges, isFileChange }
) {
if (
config.get().wakatime.isActive &&
!!config.get().wakatime.key &&
(new Date().getTime() - lastHeartbeat > 120000 || isFileChange)
) {
const notePath = path.join(storagePath, 'notes', noteKey + '.cson')
if (!isWrite && !hasFileChanges && !isFileChange) {
return
}
lastHeartbeat = new Date()
const wakatimeKey = config.get().wakatime.key
if (wakatimeKey) {
exec(
`wakatime --file ${notePath} --project '${storageName}' --key ${wakatimeKey} --plugin Boostnote-wakatime`,
(error, stdOut, stdErr) => {
if (error) {
console.log(error)
lastHeartbeat = 0
} else {
console.log(
'wakatime',
'isWrite',
isWrite,
'hasChanges',
hasFileChanges,
'isFileChange',
isFileChange
)
}
}
)
}
}
}
export { sendWakatimeHeartBeat }

View File

@@ -23,17 +23,10 @@ body[data-theme="dark"]
border-left 1px solid $ui-dark-borderColor border-left 1px solid $ui-dark-borderColor
.empty-message .empty-message
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
apply-theme(theme) body[data-theme="solarized-dark"]
body[data-theme={theme}] .root
.root background-color $ui-solarized-dark-noteDetail-backgroundColor
background-color get-theme-var(theme, 'noteDetail-backgroundColor') border-left 1px solid $ui-solarized-dark-borderColor
border-left 1px solid get-theme-var(theme, 'borderColor') .empty-message
.empty-message color $ui-solarized-dark-text-color
color get-theme-var(theme, 'text-color')
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -3,10 +3,9 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './FolderSelect.styl' import styles from './FolderSelect.styl'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class FolderSelect extends React.Component { class FolderSelect extends React.Component {
constructor(props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
@@ -16,27 +15,24 @@ class FolderSelect extends React.Component {
} }
} }
componentDidMount() { componentDidMount () {
this.value = this.props.value this.value = this.props.value
} }
componentDidUpdate() { componentDidUpdate () {
this.value = this.props.value this.value = this.props.value
} }
handleClick(e) { handleClick (e) {
this.setState( this.setState({
{ status: 'SEARCH',
status: 'SEARCH', optionIndex: -1
optionIndex: -1 }, () => {
}, this.refs.search.focus()
() => { })
this.refs.search.focus()
}
)
} }
handleFocus(e) { handleFocus (e) {
if (this.state.status === 'IDLE') { if (this.state.status === 'IDLE') {
this.setState({ this.setState({
status: 'FOCUS' status: 'FOCUS'
@@ -44,7 +40,7 @@ class FolderSelect extends React.Component {
} }
} }
handleBlur(e) { handleBlur (e) {
if (this.state.status === 'FOCUS') { if (this.state.status === 'FOCUS') {
this.setState({ this.setState({
status: 'IDLE' status: 'IDLE'
@@ -52,49 +48,40 @@ class FolderSelect extends React.Component {
} }
} }
handleKeyDown(e) { handleKeyDown (e) {
switch (e.keyCode) { switch (e.keyCode) {
case 13: case 13:
if (this.state.status === 'FOCUS') { if (this.state.status === 'FOCUS') {
this.setState( this.setState({
{ status: 'SEARCH',
status: 'SEARCH', optionIndex: -1
optionIndex: -1 }, () => {
}, this.refs.search.focus()
() => { })
this.refs.search.focus()
}
)
} }
break break
case 40: case 40:
case 38: case 38:
if (this.state.status === 'FOCUS') { if (this.state.status === 'FOCUS') {
this.setState( this.setState({
{ status: 'SEARCH',
status: 'SEARCH', optionIndex: 0
optionIndex: 0 }, () => {
}, this.refs.search.focus()
() => { })
this.refs.search.focus()
}
)
} }
break break
case 9: case 9:
if (e.shiftKey) { if (e.shiftKey) {
e.preventDefault() e.preventDefault()
const tabbable = document.querySelectorAll( const tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])')
'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])' const previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
)
const previousEl =
tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
if (previousEl != null) previousEl.focus() if (previousEl != null) previousEl.focus()
} }
} }
} }
handleSearchInputBlur(e) { handleSearchInputBlur (e) {
if (e.relatedTarget !== this.refs.root) { if (e.relatedTarget !== this.refs.root) {
this.setState({ this.setState({
status: 'IDLE' status: 'IDLE'
@@ -102,17 +89,14 @@ class FolderSelect extends React.Component {
} }
} }
handleSearchInputChange(e) { handleSearchInputChange (e) {
const { folders } = this.props const { folders } = this.props
const search = this.refs.search.value const search = this.refs.search.value
const optionIndex = const optionIndex = search.length > 0
search.length > 0 ? _.findIndex(folders, (folder) => {
? _.findIndex(folders, folder => { return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
return folder.name.match( })
new RegExp('^' + _.escapeRegExp(search), 'i') : -1
)
})
: -1
this.setState({ this.setState({
search: this.refs.search.value, search: this.refs.search.value,
@@ -120,7 +104,7 @@ class FolderSelect extends React.Component {
}) })
} }
handleSearchInputKeyDown(e) { handleSearchInputKeyDown (e) {
switch (e.keyCode) { switch (e.keyCode) {
case 40: case 40:
e.stopPropagation() e.stopPropagation()
@@ -136,18 +120,15 @@ class FolderSelect extends React.Component {
break break
case 27: case 27:
e.stopPropagation() e.stopPropagation()
this.setState( this.setState({
{ status: 'FOCUS'
status: 'FOCUS' }, () => {
}, this.refs.root.focus()
() => { })
this.refs.root.focus()
}
)
} }
} }
nextOption() { nextOption () {
let { optionIndex } = this.state let { optionIndex } = this.state
const { folders } = this.props const { folders } = this.props
@@ -159,7 +140,7 @@ class FolderSelect extends React.Component {
}) })
} }
previousOption() { previousOption () {
const { folders } = this.props const { folders } = this.props
let { optionIndex } = this.state let { optionIndex } = this.state
@@ -171,52 +152,46 @@ class FolderSelect extends React.Component {
}) })
} }
selectOption() { selectOption () {
const { folders } = this.props const { folders } = this.props
const optionIndex = this.state.optionIndex const optionIndex = this.state.optionIndex
const folder = folders[optionIndex] const folder = folders[optionIndex]
if (folder != null) { if (folder != null) {
this.setState( this.setState({
{ status: 'FOCUS'
status: 'FOCUS' }, () => {
}, this.setValue(folder.key)
() => { this.refs.root.focus()
this.setValue(folder.key) })
this.refs.root.focus()
}
)
} }
} }
handleOptionClick(storageKey, folderKey) { handleOptionClick (storageKey, folderKey) {
return e => { return (e) => {
e.stopPropagation() e.stopPropagation()
this.setState( this.setState({
{ status: 'FOCUS'
status: 'FOCUS' }, () => {
}, this.setValue(storageKey + '-' + folderKey)
() => { this.refs.root.focus()
this.setValue(storageKey + '-' + folderKey) })
this.refs.root.focus()
}
)
} }
} }
setValue(value) { setValue (value) {
this.value = value this.value = value
this.props.onChange() this.props.onChange()
} }
render() { render () {
const { className, data, value } = this.props const { className, data, value } = this.props
const splitted = value.split('-') const splitted = value.split('-')
const storageKey = splitted.shift() const storageKey = splitted.shift()
const folderKey = splitted.shift() const folderKey = splitted.shift()
let options = [] let options = []
data.storageMap.forEach((storage, index) => { data.storageMap.forEach((storage, index) => {
storage.folders.forEach(folder => { storage.folders.forEach((folder) => {
options.push({ options.push({
storage: storage, storage: storage,
folder: folder folder: folder
@@ -224,78 +199,68 @@ class FolderSelect extends React.Component {
}) })
}) })
const currentOption = options.filter( const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
option =>
option.storage.key === storageKey && option.folder.key === folderKey
)[0]
if (this.state.search.trim().length > 0) { if (this.state.search.trim().length > 0) {
const filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i') const filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
options = options.filter(option => filter.test(option.folder.name)) options = options.filter((option) => filter.test(option.folder.name))
} }
const optionList = options.map((option, index) => { const optionList = options
return ( .map((option, index) => {
<div return (
styleName={ <div styleName={index === this.state.optionIndex
index === this.state.optionIndex
? 'search-optionList-item--active' ? 'search-optionList-item--active'
: 'search-optionList-item' : 'search-optionList-item'
} }
key={option.storage.key + '-' + option.folder.key} key={option.storage.key + '-' + option.folder.key}
onClick={e => onClick={(e) => this.handleOptionClick(option.storage.key, option.folder.key)(e)}
this.handleOptionClick(option.storage.key, option.folder.key)(e)
}
>
<span
styleName='search-optionList-item-name'
style={{ borderColor: option.folder.color }}
> >
{option.folder.name} <span styleName='search-optionList-item-name'
<span styleName='search-optionList-item-name-surfix'> style={{borderColor: option.folder.color}}
in {option.storage.name} >
{option.folder.name}
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span>
</span> </span>
</span> </div>
</div> )
) })
})
return ( return (
<div <div className={_.isString(className)
className={ ? 'FolderSelect ' + className
_.isString(className) ? 'FolderSelect ' + className : 'FolderSelect' : 'FolderSelect'
} }
styleName={ styleName={this.state.status === 'SEARCH'
this.state.status === 'SEARCH' ? 'root--search'
? 'root--search' : this.state.status === 'FOCUS'
: this.state.status === 'FOCUS' ? 'root--focus'
? 'root--focus' : 'root'
: 'root'
} }
ref='root' ref='root'
tabIndex='0' tabIndex='0'
onClick={e => this.handleClick(e)} onClick={(e) => this.handleClick(e)}
onFocus={e => this.handleFocus(e)} onFocus={(e) => this.handleFocus(e)}
onBlur={e => this.handleBlur(e)} onBlur={(e) => this.handleBlur(e)}
onKeyDown={e => this.handleKeyDown(e)} onKeyDown={(e) => this.handleKeyDown(e)}
> >
{this.state.status === 'SEARCH' ? ( {this.state.status === 'SEARCH'
<div styleName='search'> ? <div styleName='search'>
<input <input styleName='search-input'
styleName='search-input'
ref='search' ref='search'
value={this.state.search} value={this.state.search}
placeholder={i18n.__('Folder...')} placeholder='Folder...'
onChange={e => this.handleSearchInputChange(e)} onChange={(e) => this.handleSearchInputChange(e)}
onBlur={e => this.handleSearchInputBlur(e)} onBlur={(e) => this.handleSearchInputBlur(e)}
onKeyDown={e => this.handleSearchInputKeyDown(e)} onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
/> />
<div styleName='search-optionList' ref='optionList'> <div styleName='search-optionList'
ref='optionList'
>
{optionList} {optionList}
</div> </div>
</div> </div>
) : currentOption ? ( : <div styleName='idle' style={{color: currentOption.folder.color}}>
<div styleName='idle' style={{ color: currentOption.folder.color }}>
<div styleName='idle-label'> <div styleName='idle-label'>
<i className='fa fa-folder' /> <i className='fa fa-folder' />
<span styleName='idle-label-name'> <span styleName='idle-label-name'>
@@ -303,7 +268,8 @@ class FolderSelect extends React.Component {
</span> </span>
</div> </div>
</div> </div>
) : null} }
</div> </div>
) )
} }
@@ -313,13 +279,11 @@ FolderSelect.propTypes = {
className: PropTypes.string, className: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
value: PropTypes.string, value: PropTypes.string,
folders: PropTypes.arrayOf( folders: PropTypes.arrayOf(PropTypes.shape({
PropTypes.shape({ key: PropTypes.string,
key: PropTypes.string, name: PropTypes.string,
name: PropTypes.string, color: PropTypes.string
color: PropTypes.string }))
})
)
} }
export default CSSModules(FolderSelect, styles) export default CSSModules(FolderSelect, styles)

View File

@@ -36,7 +36,7 @@
height 34px height 34px
width 20px width 20px
line-height 34px line-height 34px
.search-input .search-input
vertical-align middle vertical-align middle
position relative position relative
@@ -71,7 +71,7 @@
overflow ellipsis overflow ellipsis
cursor pointer cursor pointer
&:hover &:hover
background-color $ui-button--hover-backgroundColor background-color $ui-button--hover-backgroundColor
.search-optionList-item--active .search-optionList-item--active
@extend .search-optionList-item @extend .search-optionList-item
@@ -133,40 +133,3 @@ body[data-theme="dark"]
color $ui-dark-button--active-color color $ui-dark-button--active-color
.search-optionList-item-name-surfix .search-optionList-item-name-surfix
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
apply-theme(theme)
body[data-theme={theme}]
.root
&:hover
background-color get-theme-var(theme, 'button--hover-backgroundColor')
border-color get-theme-var(theme, 'borderColor')
.search-input
color get-theme-var(theme, 'text-color')
background-color transparent
border-color get-theme-var(theme, 'borderColor')
.search-optionList
color get-theme-var(theme, 'text-color')
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'button-backgroundColor')
.search-optionList-item
&:hover
background-color lighten(get-theme-var(theme, 'button--hover-backgroundColor'), 15%)
.search-optionList-item--active
background-color get-theme-var(theme, 'button--active-backgroundColor')
color get-theme-var(theme, 'button--active-color')
&:hover
background-color get-theme-var(theme, 'button--active-backgroundColor')
color get-theme-var(theme, 'button--active-color')
.search-optionList-item-name-surfix
color get-theme-var(theme, 'inactive-text-color')
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -1,71 +0,0 @@
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)

View File

@@ -1,41 +0,0 @@
.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)

View File

@@ -2,24 +2,15 @@ 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 './FullscreenButton.styl' import styles from './FullscreenButton.styl'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin' const FullscreenButton = ({
const FullscreenButton = ({ onClick }) => { onClick
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B' }) => (
return ( <button styleName='control-fullScreenButton' title='Fullscreen' onMouseDown={(e) => onClick(e)}>
<button <img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
styleName='control-fullScreenButton' <span styleName='tooltip'>Fullscreen</span>
title={i18n.__('Fullscreen')} </button>
onMouseDown={e => onClick(e)} )
>
<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

View File

@@ -1,26 +1,22 @@
.control-fullScreenButton .control-fullScreenButton
top 80px top 80px
topBarButtonRight() topBarButtonRight()
&:hover .tooltip &:hover .tooltip
opacity 1 opacity 1
.tooltip .tooltip
tooltip() tooltip()
position absolute position absolute
pointer-events none pointer-events none
top 50px top 50px
right 70px right 70px
z-index 200 z-index 200
padding 5px padding 5px
line-height normal line-height normal
border-radius 2px border-radius 2px
opacity 0 opacity 0
transition 0.1s transition 0.1s
.tooltip:lang(ja) body[data-theme="dark"]
@extend .tooltip .control-fullScreenButton
right 35px
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark() topBarButtonDark()

View File

@@ -2,12 +2,15 @@ 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 './InfoButton.styl' import styles from './InfoButton.styl'
import i18n from 'browser/lib/i18n'
const InfoButton = ({ onClick }) => ( const InfoButton = ({
<button styleName='control-infoButton' onClick={e => onClick(e)}> onClick
}) => (
<button styleName='control-infoButton'
onClick={(e) => onClick(e)}
>
<img className='infoButton' src='../resources/icon/icon-info.svg' /> <img className='infoButton' src='../resources/icon/icon-info.svg' />
<span styleName='tooltip'>{i18n.__('Info')}</span> <span styleName='tooltip'>Info</span>
</button> </button>
) )

View File

@@ -3,134 +3,90 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl' import styles from './InfoPanel.styl'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import i18n from 'browser/lib/i18n'
class InfoPanel extends React.Component { class InfoPanel extends React.Component {
copyNoteLink() { copyNoteLink () {
const { noteLink } = this.props const {noteLink} = this.props
this.refs.noteLink.select() this.refs.noteLink.select()
copy(noteLink) copy(noteLink)
} }
render() { render () {
const { const {
storageName, storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
folderName,
noteLink,
updatedAt,
createdAt,
exportAsMd,
exportAsTxt,
exportAsHtml,
exportAsPdf,
wordCount,
letterCount,
type,
print
} = this.props } = this.props
return ( return (
<div <div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
className='infoPanel'
styleName='control-infoButton-panel'
style={{ display: 'none' }}
>
<div> <div>
<p styleName='modification-date'>{updatedAt}</p> <p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'> <p styleName='modification-date-desc'>MODIFICATION DATE</p>
{i18n.__('MODIFICATION DATE')}
</p>
</div> </div>
<hr /> <hr />
{type === 'SNIPPET_NOTE' ? ( {type === 'SNIPPET_NOTE'
'' ? ''
) : ( : <div styleName='count-wrap'>
<div styleName='count-wrap'>
<div styleName='count-number'> <div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{wordCount}</p> <p styleName='infoPanel-defaul-count'>{wordCount}</p>
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p> <p styleName='infoPanel-sub-count'>Words</p>
</div> </div>
<div styleName='count-number'> <div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{letterCount}</p> <p styleName='infoPanel-defaul-count'>{letterCount}</p>
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p> <p styleName='infoPanel-sub-count'>Letters</p>
</div> </div>
</div> </div>
)} }
{type === 'SNIPPET_NOTE' ? '' : <hr />} {type === 'SNIPPET_NOTE'
? ''
: <hr />
}
<div> <div>
<p styleName='infoPanel-default'>{storageName}</p> <p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p> <p styleName='infoPanel-sub'>STORAGE</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'>{folderName}</p> <p styleName='infoPanel-default'>{folderName}</p>
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p> <p styleName='infoPanel-sub'>FOLDER</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'>{createdAt}</p> <p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p> <p styleName='infoPanel-sub'>CREATION DATE</p>
</div> </div>
<div> <div>
<input <input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
styleName='infoPanel-noteLink' <button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
ref='noteLink'
defaultValue={noteLink}
onClick={e => {
e.target.select()
}}
/>
<button
onClick={() => this.copyNoteLink()}
styleName='infoPanel-copyButton'
>
<i className='fa fa-clipboard' /> <i className='fa fa-clipboard' />
</button> </button>
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p> <p styleName='infoPanel-sub'>NOTE LINK</p>
</div> </div>
<hr /> <hr />
<div id='export-wrap'> <div id='export-wrap'>
<button <button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
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>.md</p>
</button> </button>
<button <button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
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>.txt</p>
</button> </button>
<button <button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
styleName='export--enable'
onClick={e => exportAsHtml(e, 'export-html')}
>
<i className='fa fa-html5' /> <i className='fa fa-html5' />
<p>{i18n.__('.html')}</p> <p>.html</p>
</button> </button>
<button <button styleName='export--enable' onClick={(e) => print(e)}>
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>Print</p>
</button> </button>
</div> </div>
</div> </div>
@@ -147,7 +103,6 @@ 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,

View File

@@ -11,11 +11,10 @@
.control-infoButton-panel .control-infoButton-panel
z-index 200 z-index 200
margin-top 0px margin-top 0px
top: 50px
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)
@@ -33,7 +32,6 @@
.control-infoButton-panel-trash .control-infoButton-panel-trash
z-index 200 z-index 200
margin-top 0px margin-top 0px
top 50px
right 0px right 0px
position absolute position absolute
padding 20px 25px 0 25px padding 20px 25px 0 25px
@@ -138,49 +136,82 @@
.export--unable .export--unable
cursor not-allowed cursor not-allowed
apply-theme(theme) body[data-theme="dark"]
body[data-theme={theme}] .control-infoButton-panel
.control-infoButton-panel background-color $ui-dark-noteList-backgroundColor
background-color get-theme-var(theme, 'noteList-backgroundColor')
.control-infoButton-panel-trash .control-infoButton-panel-trash
background-color get-theme-var(theme, 'noteList-backgroundColor') background-color $ui-dark-noteList-backgroundColor
.modification-date .modification-date
color get-theme-var(theme, 'text-color') color $ui-dark-text-color
.modification-date-desc .modification-date-desc
color $ui-inactive-text-color color $ui-inactive-text-color
.infoPanel-defaul-count .infoPanel-defaul-count
color get-theme-var(theme, 'text-color') color $ui-dark-text-color
.infoPanel-sub-count .infoPanel-sub-count
color $ui-inactive-text-color color $ui-inactive-text-color
.infoPanel-default .infoPanel-default
color get-theme-var(theme, 'text-color') color $ui-dark-text-color
.infoPanel-sub .infoPanel-sub
color $ui-inactive-text-color color $ui-inactive-text-color
.infoPanel-noteLink .infoPanel-noteLink
background-color alpha(get-theme-var(theme, 'borderColor'), 20%) background-color alpha($ui-dark-borderColor, 60%)
color get-theme-var(theme, 'text-color') color $ui-dark-text-color
[id=export-wrap] [id=export-wrap]
button button
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
&:hover &:hover
background-color alpha(get-theme-var(theme, 'borderColor'), 20%) background-color alpha($ui-dark-borderColor, 20%)
color get-theme-var(theme, 'text-color') color $ui-dark-text-color
p p
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
&:hover &:hover
color get-theme-var(theme, 'text-color') color $ui-dark-text-color
for theme in 'dark' 'solarized-dark' 'dracula' body[data-theme="solarized-dark"]
apply-theme(theme) .control-infoButton-panel
background-color $ui-solarized-dark-noteList-backgroundColor
for theme in $themes .control-infoButton-panel-trash
apply-theme(theme) background-color $ui-solarized-ark-noteList-backgroundColor
.modification-date
color $ui-solarized-ark-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-solarized-dark-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-solarized-ark-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-solarized-dark-borderColor, 20%)
color $ui-solarized-dark-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-solarized-dark-borderColor, 20%)
color $ui-solarized-ark-text-color
p
color $ui-dark-inactive-text-color
&:hover
color $ui-solarized-ark-text-color

View File

@@ -2,77 +2,50 @@ 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 './InfoPanel.styl' import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n'
const InfoPanelTrashed = ({ const InfoPanelTrashed = ({
storageName, storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
folderName,
updatedAt,
createdAt,
exportAsMd,
exportAsTxt,
exportAsHtml,
exportAsPdf
}) => ( }) => (
<div <div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
className='infoPanel'
styleName='control-infoButton-panel-trash'
style={{ display: 'none' }}
>
<div> <div>
<p styleName='modification-date'>{updatedAt}</p> <p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p> <p styleName='modification-date-desc'>MODIFICATION DATE</p>
</div> </div>
<hr /> <hr />
<div> <div>
<p styleName='infoPanel-default'>{storageName}</p> <p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p> <p styleName='infoPanel-sub'>STORAGE</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'> <p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
<text styleName='infoPanel-trash'>Trash</text> <p styleName='infoPanel-sub'>FOLDER</p>
{folderName}
</p>
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'>{createdAt}</p> <p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p> <p styleName='infoPanel-sub'>CREATION DATE</p>
</div> </div>
<div id='export-wrap'> <div id='export-wrap'>
<button <button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
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 <button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
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 <button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
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 <button styleName='export--unable'>
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>
@@ -87,8 +60,7 @@ 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)

659
browser/main/Detail/MarkdownNoteDetail.js Executable file → Normal file
View File

@@ -1,4 +1,3 @@
/* eslint-disable camelcase */
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'
@@ -10,6 +9,7 @@ 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'
@@ -19,7 +19,6 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import TrashButton from './TrashButton' import TrashButton from './TrashButton'
import FullscreenButton from './FullscreenButton' import FullscreenButton from './FullscreenButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton' import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton' import InfoButton from './InfoButton'
import ToggleModeButton from './ToggleModeButton' import ToggleModeButton from './ToggleModeButton'
@@ -28,149 +27,98 @@ import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter' import { formatDate } from 'browser/lib/date-formatter'
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags' import striptags from 'striptags'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import markdownToc from 'browser/lib/markdown-toc-generator'
import queryString from 'query-string'
import { replace } from 'connected-react-router'
import ToggleDirectionButton from 'browser/main/Detail/ToggleDirectionButton'
class MarkdownNoteDetail extends React.Component { class MarkdownNoteDetail extends React.Component {
constructor(props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
isMovingNote: false, isMovingNote: false,
note: Object.assign( note: Object.assign({
{ title: '',
title: '', content: ''
content: '', }, props.note),
linesHighlighted: [] isLockButtonShown: false,
},
props.note
),
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,
RTL: false
} }
this.dispatchTimer = null this.dispatchTimer = null
this.generateToc = this.handleGenerateToc.bind(this)
this.toggleLockButton = this.handleToggleLockButton.bind(this) this.toggleLockButton = this.handleToggleLockButton.bind(this)
this.handleUpdateContent = this.handleUpdateContent.bind(this)
this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this)
this.getNote = this.getNote.bind(this)
} }
focus() { focus () {
this.refs.content.focus() this.refs.content.focus()
} }
componentDidMount() { componentDidMount () {
ee.on('editor:orientation', this.handleSwitchStackDirection)
ee.on('topbar:togglelockbutton', this.toggleLockButton) ee.on('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection())
ee.on('topbar:togglemodebutton', () => {
const reversedType =
this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType)
})
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
ee.on('code:generate-toc', this.generateToc)
} }
UNSAFE_componentWillReceiveProps(nextProps) { componentWillReceiveProps (nextProps) {
const isNewNote = nextProps.note.key !== this.props.note.key if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
const hasDeletedTags =
nextProps.note.tags.length < this.props.note.tags.length
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow() if (this.saveQueue != null) this.saveNow()
this.setState(
{
note: Object.assign({ linesHighlighted: [] }, nextProps.note)
},
() => {
this.refs.content.reload()
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({ this.setState({
switchPreview note: Object.assign({}, nextProps.note)
}, () => {
this.refs.content.reload()
if (this.refs.tags) this.refs.tags.reset()
}) })
if (switchPreview === 'BLUR' || switchPreview === 'DBL_CLICK') {
console.log('setting focus', switchPreview)
this.focus()
}
} }
} }
componentWillUnmount() { componentWillUnmount () {
ee.off('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', this.handleSwitchDirection)
ee.off('code:generate-toc', this.generateToc)
if (this.saveQueue != null) this.saveNow() if (this.saveQueue != null) this.saveNow()
} }
handleUpdateTag() { componentDidUnmount () {
ee.off('topbar:togglelockbutton', this.toggleLockButton)
}
handleUpdateTag () {
const { note } = this.state const { note } = this.state
if (this.refs.tags) note.tags = this.refs.tags.value if (this.refs.tags) note.tags = this.refs.tags.value
this.updateNote(note) this.updateNote(note)
} }
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)))
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)
} }
updateNote(note) { updateNote (note) {
note.updatedAt = new Date() note.updatedAt = new Date()
this.setState({ note }, () => { this.setState({note}, () => {
this.save() this.save()
}) })
} }
save() { save () {
clearTimeout(this.saveQueue) clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => { this.saveQueue = setTimeout(() => {
this.saveNow() this.saveNow()
}, 1000) }, 1000)
} }
saveNow() { saveNow () {
const { note, dispatch } = this.props const { note, dispatch } = this.props
clearTimeout(this.saveQueue) clearTimeout(this.saveQueue)
this.saveQueue = null this.saveQueue = null
dataApi.updateNote(note.storage, note.key, this.state.note).then(note => { dataApi
dispatch({ .updateNote(note.storage, note.key, this.state.note)
type: 'UPDATE_NOTE', .then((note) => {
note: note dispatch({
type: 'UPDATE_NOTE',
note: note
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
}) })
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
})
} }
handleFolderChange(e) { handleFolderChange (e) {
const { note } = this.state const { note } = this.state
const value = this.refs.folder.value const value = this.refs.folder.value
const splitted = value.split('-') const splitted = value.split('-')
@@ -179,114 +127,70 @@ class MarkdownNoteDetail extends React.Component {
dataApi dataApi
.moveNote(note.storage, note.key, newStorageKey, newFolderKey) .moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then(newNote => { .then((newNote) => {
this.setState( this.setState({
{ isMovingNote: true,
isMovingNote: true, note: Object.assign({}, newNote)
note: Object.assign({}, newNote) }, () => {
}, const { dispatch, location } = this.props
() => { dispatch({
const { dispatch, location } = this.props type: 'MOVE_NOTE',
dispatch({ originNote: note,
type: 'MOVE_NOTE', note: newNote
originNote: note, })
note: newNote hashHistory.replace({
}) pathname: location.pathname,
dispatch( query: {
replace({ key: newNote.storage + '-' + newNote.key
pathname: location.pathname, }
search: queryString.stringify({ })
key: newNote.key this.setState({
}) isMovingNote: false
}) })
) })
this.setState({
isMovingNote: false
})
}
)
}) })
} }
handleStarButtonClick(e) { handleStarButtonClick (e) {
const { note } = this.state const { note } = this.state
if (!note.isStarred) if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
note.isStarred = !note.isStarred note.isStarred = !note.isStarred
this.setState( this.setState({
{ note
note }, () => {
}, this.save()
() => { })
this.save()
}
)
} }
exportAsFile() {} exportAsFile () {
exportAsMd() { }
exportAsMd () {
ee.emit('export:save-md') ee.emit('export:save-md')
} }
exportAsTxt() { exportAsTxt () {
ee.emit('export:save-text') ee.emit('export:save-text')
} }
exportAsHtml() { exportAsHtml () {
ee.emit('export:save-html') ee.emit('export:save-html')
} }
exportAsPdf() { handleTrashButtonClick (e) {
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) {
const { note } = this.state const { note } = this.state
const { isTrashed } = note const { isTrashed } = note
const { confirmDeletion } = this.props.config.ui const { confirmDeletion } = this.props
if (isTrashed) { if (isTrashed) {
if (confirmDeleteNote(confirmDeletion, true)) { if (confirmDeletion(true)) {
const { note, dispatch } = this.props const {note, dispatch} = this.props
dataApi dataApi
.deleteNote(note.storage, note.key) .deleteNote(note.storage, note.key)
.then(data => { .then((data) => {
const dispatchHandler = () => { const dispatchHandler = () => {
dispatch({ dispatch({
type: 'DELETE_NOTE', type: 'DELETE_NOTE',
@@ -294,348 +198,232 @@ class MarkdownNoteDetail extends React.Component {
noteKey: data.noteKey noteKey: data.noteKey
}) })
} }
ee.once('list:next', dispatchHandler) ee.once('list:moved', dispatchHandler)
}) })
.then(() => ee.emit('list:next'))
} }
} else { } else {
if (confirmDeleteNote(confirmDeletion, false)) { if (confirmDeletion()) {
note.isTrashed = true note.isTrashed = true
this.setState( this.setState({
{ note
note }, () => {
}, this.save()
() => { })
this.save()
}
)
ee.emit('list:next') ee.emit('list:next')
} }
} }
} }
handleUndoButtonClick(e) { handleUndoButtonClick (e) {
const { note } = this.state const { note } = this.state
note.isTrashed = false note.isTrashed = false
this.setState( this.setState({
{ note
note }, () => {
}, this.save()
() => { this.refs.content.reload()
this.save() ee.emit('list:next')
this.refs.content.reload() })
ee.emit('list:next')
}
)
} }
handleFullScreenButton(e) { handleFullScreenButton (e) {
ee.emit('editor:fullscreen') ee.emit('editor:fullscreen')
} }
handleLockButtonMouseDown(e) { handleLockButtonMouseDown (e) {
e.preventDefault() e.preventDefault()
ee.emit('editor:lock') ee.emit('editor:lock')
this.setState({ isLocked: !this.state.isLocked }) this.setState({ isLocked: !this.state.isLocked })
if (this.state.isLocked) this.focus() if (this.state.isLocked) this.focus()
} }
getToggleLockButton() { getToggleLockButton () {
return this.state.isLocked return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
? '../resources/icon/icon-lock.svg'
: '../resources/icon/icon-unlock.svg'
} }
handleDeleteKeyDown(e) { handleDeleteKeyDown (e) {
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e) if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
} }
handleToggleLockButton(event, noteStatus) { handleToggleLockButton (event, noteStatus) {
// first argument event is not used // first argument event is not used
if (noteStatus === 'CODE') { if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
this.setState({ isLockButtonShown: true }) this.setState({isLockButtonShown: true})
} else { } else {
this.setState({ isLockButtonShown: false }) this.setState({isLockButtonShown: false})
} }
} }
handleGenerateToc() { handleFocus (e) {
const editor = this.refs.content.refs.code.editor
markdownToc.generateInEditor(editor)
}
handleFocus(e) {
this.focus() this.focus()
} }
handleInfoButtonClick(e) { handleInfoButtonClick (e) {
const infoPanel = document.querySelector('.infoPanel') const infoPanel = document.querySelector('.infoPanel')
if (infoPanel.style) if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
infoPanel.style.display =
infoPanel.style.display === 'none' ? 'inline' : 'none'
} }
print(e) { print (e) {
ee.emit('print') ee.emit('print')
} }
handleSwitchMode(type) { handleSwitchMode (type) {
// If in split mode, hide the lock button this.setState({ editorType: type }, () => {
this.setState( const newConfig = Object.assign({}, this.props.config)
{ editorType: type, isLockButtonShown: type !== 'SPLIT' }, newConfig.editor.type = type
() => { ConfigManager.set(newConfig)
this.focus() })
const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type
ConfigManager.set(newConfig)
}
)
} }
handleSwitchStackDirection() { renderEditor () {
this.setState(
prevState => ({ isStacking: !prevState.isStacking }),
() => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.ui.isStacking = this.state.isStacking
ConfigManager.set(newConfig)
}
)
}
handleSwitchDirection() {
if (!this.props.config.editor.rtlEnabled) {
return
}
// If in split mode, hide the lock button
const direction = this.state.RTL
this.setState({ RTL: !direction })
}
handleDeleteNote() {
this.handleTrashButtonClick()
}
handleClearTodo() {
const { note } = this.state
const splitted = note.content.split('\n')
const clearTodoContent = splitted
.map(line => {
const trimmedLine = line.trim()
if (trimmedLine.match(/\[x\]/i)) {
return line.replace(/\[x\]/i, '[ ]')
} else {
return line
}
})
.join('\n')
note.content = clearTodoContent
this.refs.content.setValue(note.content)
this.updateNote(note)
}
getNote() {
return this.state.note
}
renderEditor() {
const { config, ignorePreviewPointerEvents } = this.props const { config, ignorePreviewPointerEvents } = this.props
const { note, isStacking } = this.state const { note } = this.state
if (this.state.editorType === 'EDITOR_PREVIEW') { if (this.state.editorType === 'EDITOR_PREVIEW') {
return ( return <MarkdownEditor
<MarkdownEditor ref='content'
ref='content' styleName='body-noteEditor'
styleName='body-noteEditor' config={config}
config={config} value={note.content}
value={note.content} storageKey={note.storage}
storageKey={note.storage} onChange={this.handleUpdateContent.bind(this)}
noteKey={note.key} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
linesHighlighted={note.linesHighlighted} />
onChange={this.handleUpdateContent}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
RTL={config.editor.rtlEnabled && this.state.RTL}
/>
)
} else { } else {
return ( return <MarkdownSplitEditor
<MarkdownSplitEditor ref='content'
ref='content' config={config}
config={config} value={note.content}
value={note.content} storageKey={note.storage}
storageKey={note.storage} onChange={this.handleUpdateContent.bind(this)}
noteKey={note.key} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
isStacking={isStacking} />
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
RTL={config.editor.rtlEnabled && this.state.RTL}
/>
)
} }
} }
render() { render () {
const { data, dispatch, location, config } = this.props const { data, location } = 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
const options = [] const options = []
data.storageMap.forEach((storage, index) => { data.storageMap.forEach((storage, index) => {
storage.folders.forEach(folder => { storage.folders.forEach((folder) => {
options.push({ options.push({
storage: storage, storage: storage,
folder: folder folder: folder
}) })
}) })
}) })
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
const currentOption = _.find( const trashTopBar = <div styleName='info'>
options, <div styleName='info-left'>
option => <i styleName='undo-button'
option.storage.key === storageKey && option.folder.key === folderKey className='fa fa-undo fa-fw'
) onClick={(e) => this.handleUndoButtonClick(e)}
/>
// currentOption may be undefined
const storageName = _.get(currentOption, 'storage.name') || ''
const folderName = _.get(currentOption, 'folder.name') || ''
const trashTopBar = (
<div styleName='info'>
<div styleName='info-left'>
<RestoreButton onClick={e => this.handleUndoButtonClick(e)} />
</div>
<div styleName='info-right'>
<PermanentDeleteButton
onClick={e => this.handleTrashButtonClick(e)}
/>
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanelTrashed
storageName={storageName}
folderName={folderName}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsHtml={this.exportAsHtml}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsPdf={this.exportAsPdf}
/>
</div>
</div> </div>
) <div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanelTrashed
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsHtml={this.exportAsHtml}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
/>
</div>
</div>
const detailTopBar = ( const detailTopBar = <div styleName='info'>
<div styleName='info'> <div styleName='info-left'>
<div styleName='info-left'> <div styleName='info-left-top'>
<div> <FolderSelect styleName='info-left-top-folderSelect'
<FolderSelect value={this.state.note.storage + '-' + this.state.note.folder}
styleName='info-left-top-folderSelect' ref='folder'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
data={data}
onChange={e => this.handleFolderChange(e)}
/>
</div>
<TagSelect
ref='tags'
value={this.state.note.tags}
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data} data={data}
dispatch={dispatch} onChange={(e) => this.handleFolderChange(e)}
onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags}
/>
<TodoListPercentage
onClearCheckboxClick={e => this.handleClearTodo(e)}
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
/> />
</div> </div>
<div styleName='info-right'>
<ToggleModeButton
onClick={e => this.handleSwitchMode(e)}
editorType={editorType}
/>
{this.props.config.editor.rtlEnabled && (
<ToggleDirectionButton
onClick={e => this.handleSwitchDirection(e)}
isRTL={this.state.RTL}
/>
)}
<StarButton
onClick={e => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
{(() => { <TagSelect
const imgSrc = `${this.getToggleLockButton()}` ref='tags'
const lockButtonComponent = ( value={this.state.note.tags}
<button onChange={this.handleUpdateTag.bind(this)}
styleName='control-lockButton' />
onFocus={e => this.handleFocus(e)}
onMouseDown={e => this.handleLockButtonMouseDown(e)}
>
<img src={imgSrc} />
{this.state.isLocked ? (
<span styleName='tooltip'>Unlock</span>
) : (
<span styleName='tooltip'>Lock</span>
)}
</button>
)
return this.state.isLockButtonShown ? lockButtonComponent : '' <ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
})()}
<FullscreenButton onClick={e => this.handleFullScreenButton(e)} /> <TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
<TrashButton onClick={e => this.handleTrashButtonClick(e)} />
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanel
storageName={storageName}
folderName={folderName}
noteLink={`[${note.title}](:note:${
queryString.parse(location.search).key
})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml}
exportAsPdf={this.exportAsPdf}
wordCount={note.content.trim().split(/\s+/g).length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
print={this.print}
/>
</div>
</div> </div>
) <div styleName='info-right'>
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
{(() => {
const imgSrc = `${this.getToggleLockButton()}`
const lockButtonComponent =
<button styleName='control-lockButton'
onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<img styleName='iconInfo' src={imgSrc} />
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
</button>
return (
this.state.isLockButtonShown ? lockButtonComponent : ''
)
})()}
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml}
wordCount={note.content.split(' ').length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
print={this.print}
/>
</div>
</div>
return ( return (
<div <div className='NoteDetail'
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}
<div styleName='body'>{this.renderEditor()}</div> <div styleName='body'>
{this.renderEditor()}
</div>
<StatusBar <StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])} {..._.pick(this.props, ['config', 'location', 'dispatch'])}
@@ -649,11 +437,14 @@ class MarkdownNoteDetail extends React.Component {
MarkdownNoteDetail.propTypes = { MarkdownNoteDetail.propTypes = {
dispatch: PropTypes.func, dispatch: PropTypes.func,
repositories: PropTypes.array, repositories: PropTypes.array,
note: PropTypes.shape({}), note: PropTypes.shape({
}),
style: PropTypes.shape({ style: PropTypes.shape({
left: PropTypes.number left: PropTypes.number
}), }),
ignorePreviewPointerEvents: PropTypes.bool ignorePreviewPointerEvents: PropTypes.bool,
confirmDeletion: PropTypes.bool.isRequired
} }
export default CSSModules(MarkdownNoteDetail, styles) export default CSSModules(MarkdownNoteDetail, styles)

View File

@@ -7,7 +7,6 @@
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
box-shadow none box-shadow none
padding 20px 40px padding 20px 40px
overflow hidden
.lock-button .lock-button
padding-bottom 3px padding-bottom 3px
@@ -15,7 +14,7 @@
.control-lockButton .control-lockButton
topBarButtonRight() topBarButtonRight()
position absolute position absolute
right 265px right 225px
&:hover .tooltip &:hover .tooltip
opacity 1 opacity 1
@@ -45,7 +44,7 @@
margin 0 30px margin 0 30px
.body-noteEditor .body-noteEditor
absolute top bottom left right absolute top bottom left right
body[data-theme="white"] body[data-theme="white"]
.root .root
box-shadow $note-detail-box-shadow box-shadow $note-detail-box-shadow
@@ -66,14 +65,8 @@ body[data-theme="dark"]
.control-fullScreenButton .control-fullScreenButton
topBarButtonDark() topBarButtonDark()
apply-theme(theme)
body[data-theme={theme}]
.root
border-left 1px solid get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
for theme in 'solarized-dark' 'dracula' body[data-theme="solarized-dark"]
apply-theme(theme) .root
border-left 1px solid $ui-solarized-dark-borderColor
for theme in $themes background-color $ui-solarized-dark-noteDetail-backgroundColor
apply-theme(theme)

Some files were not shown because too many files have changed in this diff Show More