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

Merge branch 'master' into fix-autocomplete-codeblock

This commit is contained in:
Baptiste Augrain
2020-06-26 02:40:37 +02:00
322 changed files with 21618 additions and 12667 deletions

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"extends": ["standard", "standard-jsx", "plugin:react/recommended"], "extends": ["standard", "standard-jsx", "plugin:react/recommended", "prettier"],
"plugins": ["react"], "plugins": ["react", "prettier"],
"rules": { "rules": {
"no-useless-escape": 0, "no-useless-escape": 0,
"prefer-const": ["warn", { "prefer-const": ["warn", {
@@ -13,12 +13,15 @@
"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": { "env": {
"jest": true "jest": true

1
.github/FUNDING.yml vendored Normal file
View File

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

4
.gitignore vendored
View File

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

5
.prettierrc Normal file
View File

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

View File

@@ -1,10 +1,9 @@
language: node_js language: node_js
node_js: node_js:
- 7 - 8
script: script:
- npm run lint && npm run test - npm run lint && npm run test
- yarn jest - 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@6.4 && grunt pre-build; fi'
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
after_success: after_success:
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv - openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
-in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d -in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d

View File

@@ -5,19 +5,19 @@ Let us know what is currently happening.
Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug. Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug.
If your issue is regarding Boostnote mobile, please open an issue in the Boostnote Mobile repo 👉 https://github.com/BoostIO/boostnote-mobile. 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 # Expected behavior
<!-- <!--
Let us know what you think should happen! Let us know what you think should happen.
--> -->
# Steps to reproduce # Steps to reproduce
<!-- <!--
Please be thorough, issues we can reproduce are easier to fix! Please be thorough, issues we can reproduce are easier to fix.
--> -->
1. 1.
@@ -26,8 +26,8 @@ Please be thorough, issues we can reproduce are easier to fix!
# Environment # Environment
- Version : - Boostnote version: <!-- 0.x.x -->
- OS Version and name : - OS version and name: <!-- Windows 10 / Ubuntu 18.04 / etc -->
<!-- <!--
Love Boostnote? Please consider supporting us on IssueHunt: Love Boostnote? Please consider supporting us on IssueHunt:

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 - 2018 BoostIO Copyright (C) 2017 - 2019 BoostIO
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by

View File

@@ -3,13 +3,16 @@ Before submitting this PR, please make sure that:
- You have read and understand the contributing.md - You have read and understand the contributing.md
- You have checked docs/code_style.md for information on code style - You have checked docs/code_style.md for information on code style
--> -->
## Description ## Description
<!-- <!--
Tell us what your PR does. Tell us what your PR does.
Please attach a screenshot/ video/gif image describing your PR if possible. Please attach a screenshot/ video/gif image describing your PR if possible.
--> -->
## Issue fixed ## Issue fixed
<!-- <!--
Please list out all issue fixed with this PR here. Please list out all issue fixed with this PR here.
--> -->
@@ -20,6 +23,7 @@ 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 Change :white_circle: to :radio_button: in all the options that apply
--> -->
## Type of changes ## Type of changes
- :white_circle: Bug fix (Change that fixed an issue) - :white_circle: Bug fix (Change that fixed an issue)
@@ -34,3 +38,5 @@ Change :white_circle: to :radio_button: in all the options that apply
- :white_circle: I have written test for my code and it has been tested - :white_circle: I have written test for my code and it has been tested
- :white_circle: All existing tests have been passed - :white_circle: All existing tests have been passed
- :white_circle: I have attached a screenshot/video to visualize my change if possible - :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

File diff suppressed because it is too large Load Diff

View File

@@ -3,4 +3,3 @@
.spellcheck-select .spellcheck-select
border: none border: none
text-decoration underline wavy red

View File

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

@@ -0,0 +1,39 @@
.colorPicker
position fixed
z-index 2
display flex
flex-direction column
.cover
position fixed
top 0
right 0
bottom 0
left 0
.footer
display flex
justify-content center
z-index 2
align-items center
& > button + button
margin-left 10px
.btn-cancel,
.btn-confirm,
.btn-reset
vertical-align middle
height 25px
margin-top 2.5px
border-radius 2px
border none
padding 0 5px
background-color $default-button-background
&:hover
background-color $default-button-background--hover
.btn-confirm
background-color #1EC38B
&:hover
background-color darken(#1EC38B, 25%)

View File

@@ -1,3 +1,4 @@
/* 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,7 +11,7 @@ import ConfigManager from 'browser/main/lib/ConfigManager'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' 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
@@ -20,194 +21,246 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186] this.supportMdSelectionBold = [16, 17, 186]
this.state = { this.state = {
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW', status:
props.config.editor.switchPreview === 'RIGHTCLICK'
? props.config.editor.delfaultStatus
: 'CODE',
renderValue: props.value, renderValue: props.value,
keyPressed: new Set(), keyPressed: new Set(),
isLocked: false isLocked: props.isLocked
} }
this.lockEditorCode = () => this.handleLockEditor() this.lockEditorCode = this.handleLockEditor.bind(this)
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
} }
componentWillReceiveProps (props) { UNSAFE_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)
} }
queueRendering (value) { focusEditor() {
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) { setValue(value) {
this.refs.code.setValue(value) this.refs.code.setValue(value)
} }
handleChange (e) { 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' ? 'CODE' : 'PREVIEW'
this.setState({ this.setState(
status: newStatus {
}, () => { status: newStatus
if (newStatus === 'CODE') { },
this.refs.code.focus() () => {
} else { if (newStatus === 'CODE') {
this.refs.preview.focus() this.refs.code.focus()
} } else {
eventEmitter.emit('topbar:togglelockbutton', this.state.status) this.previewRef.current.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
const newConfig = Object.assign({}, config) const newConfig = Object.assign({}, config)
newConfig.editor.delfaultStatus = newStatus newConfig.editor.delfaultStatus = newStatus
ConfigManager.set(newConfig) ConfigManager.set(newConfig)
}) }
)
} }
} }
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 (config.editor.switchPreview === 'BLUR' || if (
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE') 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.scrollToRow(cursorPosition.line)
}
)
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
} }
} }
handleDoubleClick (e) { handleDoubleClick(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 (config.editor.switchPreview === 'DBL_CLICK') { if (config.editor.switchPreview === 'DBL_CLICK') {
this.setState({ this.setState(
status: 'CODE' {
}, () => { status: 'CODE'
this.refs.code.focus() },
eventEmitter.emit('topbar:togglelockbutton', this.state.status) () => {
}) this.refs.code.focus()
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
)
} }
} }
handlePreviewMouseDown (e) { handlePreviewMouseDown(e) {
this.previewMouseDownedAt = new Date() this.previewMouseDownedAt = new Date()
} }
handlePreviewMouseUp (e) { handlePreviewMouseUp(e) {
const { config } = this.props const { config } = this.props
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) { if (
this.setState({ config.editor.switchPreview === 'BLUR' &&
status: 'CODE' new Date() - this.previewMouseDownedAt < 200
}, () => { ) {
this.refs.code.focus() this.setState(
}) {
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*[\+\-\*] \[x\]/i const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/ const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
const checkReplace = /\[x\]/i const checkReplace = /\[x]/i
const uncheckReplace = /\[ \]/ const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) { if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lineIndex =
const lines = this.refs.code.value parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
.split('\n') const lines = this.refs.code.value.split('\n')
const targetLine = lines[lineIndex] const targetLine = lines[lineIndex]
let newLine = targetLine
if (targetLine.match(checkedMatch)) { if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') newLine = targetLine.replace(checkReplace, '[ ]')
} }
if (targetLine.match(uncheckedMatch)) { if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') newLine = targetLine.replace(uncheckReplace, '[x]')
} }
this.refs.code.setValue(lines.join('\n')) this.refs.code.setLineContent(lineIndex, newLine)
} }
} }
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) => { return keyPressed.has(el) } const isNoteHandlerKey = el => {
return keyPressed.has(el)
}
// These conditions are for ctrl-e and ctrl-w // These conditions are for ctrl-e and ctrl-w
if (keyPressed.size === this.escapeFromEditor.length && if (
!this.state.isLocked && this.state.status === 'CODE' && keyPressed.size === this.escapeFromEditor.length &&
this.escapeFromEditor.every(isNoteHandlerKey)) { !this.state.isLocked &&
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 (keyPressed.size === this.supportMdSelectionBold.length && this.supportMdSelectionBold.every(isNoteHandlerKey)) { if (
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)
} }
@@ -215,47 +268,63 @@ 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, { line: word.head.line, ch: word.head.ch + mdElement.length }) cmDoc.replaceRange(mdElement, {
} line: word.head.line,
ch: word.head.ch + mdElement.length
addMdAroundSelection (mdElement) {
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
}
handleDropImage (dropEvent) {
dropEvent.preventDefault()
const { storageKey, noteKey } = this.props
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
this.refs.code.editor.execCommand('goDocEnd')
this.refs.code.editor.execCommand('goLineEnd')
this.refs.code.editor.execCommand('newlineAndIndent')
attachmentManagement.handleAttachmentDrop(
this.refs.code,
storageKey,
noteKey,
dropEvent
)
}) })
} }
handleKeyUp (e) { addMdAroundSelection(mdElement) {
this.refs.code.editor.replaceSelection(
`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`
)
}
handleDropImage(dropEvent) {
dropEvent.preventDefault()
const { storageKey, noteKey } = this.props
this.setState(
{
status: 'CODE'
},
() => {
this.refs.code.focus()
this.refs.code.editor.execCommand('goDocEnd')
this.refs.code.editor.execCommand('goLineEnd')
this.refs.code.editor.execCommand('newlineAndIndent')
attachmentManagement.handleAttachmentDrop(
this.refs.code,
storageKey,
noteKey,
dropEvent
)
}
)
}
handleKeyUp(e) {
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 {className, value, config, storageKey, noteKey, linesHighlighted} = this.props const {
className,
value,
config,
storageKey,
noteKey,
linesHighlighted,
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
@@ -263,23 +332,24 @@ 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) previewStyle.pointerEvents = 'none' if (this.props.ignorePreviewPointerEvents)
previewStyle.pointerEvents = 'none'
const storage = findStorage(storageKey) const storage = findStorage(storageKey)
return ( return (
<div className={className == null <div
? 'MarkdownEditor' className={
: `MarkdownEditor ${className}` className == null ? 'MarkdownEditor' : `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 styleName={this.state.status === 'CODE' <CodeEditor
? 'codeEditor' styleName={
: 'codeEditor--hide' this.state.status === 'CODE' ? 'codeEditor' : 'codeEditor--hide'
} }
ref='code' ref='code'
mode='Boost Flavored Markdown' mode='Boost Flavored Markdown'
@@ -293,10 +363,15 @@ class MarkdownEditor extends React.Component {
enableRulers={config.editor.enableRulers} enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers} rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs} codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples} codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs} codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
@@ -305,16 +380,22 @@ class MarkdownEditor extends React.Component {
fetchUrlTitle={config.editor.fetchUrlTitle} fetchUrlTitle={config.editor.fetchUrlTitle}
enableTableEditor={config.editor.enableTableEditor} enableTableEditor={config.editor.enableTableEditor}
linesHighlighted={linesHighlighted} linesHighlighted={linesHighlighted}
onChange={(e) => this.handleChange(e)} onChange={e => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)} onBlur={e => this.handleBlur(e)}
spellCheck={config.editor.spellcheck} spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste} enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey} hotkey={config.hotkey}
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
prettierConfig={config.editor.prettierConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL}
/> />
<MarkdownPreview styleName={this.state.status === 'PREVIEW' <MarkdownPreview
? 'preview' ref={this.previewRef}
: 'preview--hide' styleName={
this.state.status === 'PREVIEW' ? 'preview' : 'preview--hide'
} }
style={previewStyle} style={previewStyle}
theme={config.ui.theme} theme={config.ui.theme}
@@ -330,21 +411,22 @@ class MarkdownEditor extends React.Component {
smartArrows={config.preview.smartArrows} smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks} breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
ref='preview' mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
onContextMenu={(e) => this.handleContextMenu(e)} onContextMenu={e => this.handleContextMenu(e)}
onDoubleClick={(e) => this.handleDoubleClick(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} noteKey={noteKey}
customCSS={config.preview.customCSS} customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS} allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox} lineThroughCheckbox={config.preview.lineThroughCheckbox}
onDrop={(e) => this.handleDropImage(e)} onDrop={e => this.handleDropImage(e)}
RTL={RTL}
/> />
</div> </div>
) )

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ 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()
@@ -16,23 +16,27 @@ class MarkdownSplitEditor extends React.Component {
this.userScroll = true this.userScroll = true
this.state = { this.state = {
isSliderFocused: false, isSliderFocused: false,
codeEditorWidthInPercent: 50 codeEditorWidthInPercent: 50,
codeEditorHeightInPercent: 50
} }
} }
setValue (value) { setValue(value) {
this.refs.code.setValue(value) this.refs.code.setValue(value)
} }
handleOnChange (e) { handleOnChange(e) {
this.value = this.refs.code.value this.value = this.refs.code.value
this.props.onChange(e) this.props.onChange(e)
} }
handleScroll (e) { handleScroll(e) {
if (!this.props.config.preview.scrollSync) return if (!this.props.config.preview.scrollSync) return
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document') const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc') const codeDoc = _.get(this, 'refs.code.editor.doc')
let srcTop, srcHeight, targetTop, targetHeight let srcTop, srcHeight, targetTop, targetHeight
@@ -49,7 +53,7 @@ class MarkdownSplitEditor extends React.Component {
targetHeight = _.get(codeDoc, 'height') targetHeight = _.get(codeDoc, 'height')
} }
const distance = (targetHeight * srcTop / srcHeight) - targetTop const distance = (targetHeight * srcTop) / srcHeight - targetTop
const framerate = 1000 / 60 const framerate = 1000 / 60
const frames = 20 const frames = 20
const refractory = frames * framerate const refractory = frames * framerate
@@ -60,114 +64,213 @@ class MarkdownSplitEditor extends React.Component {
let scrollPos, time let scrollPos, time
const timer = setInterval(() => { const timer = setInterval(() => {
time = frame / frames time = frame / frames
scrollPos = time < 0.5 scrollPos =
? 2 * time * time // ease in time < 0.5
: -1 + (4 - 2 * time) * time // ease out ? 2 * time * time // ease in
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance) : -1 + (4 - 2 * time) * time // ease out
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance) if (e.doc)
_.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
else
_.get(this, 'refs.code.editor').scrollTo(
0,
targetTop + scrollPos * distance
)
if (frame >= frames) { if (frame >= frames) {
clearInterval(timer) clearInterval(timer)
setTimeout(() => { this.userScroll = true }, refractory) setTimeout(() => {
this.userScroll = true
}, refractory)
} }
frame++ frame++
}, framerate) }, framerate)
} }
} }
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*[\+\-\*] \[x\]/i const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/ const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
const checkReplace = /\[x\]/i const checkReplace = /\[x]/i
const uncheckReplace = /\[ \]/ const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) { if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lineIndex =
const lines = this.refs.code.value parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
.split('\n') const lines = this.refs.code.value.split('\n')
const targetLine = lines[lineIndex] const targetLine = lines[lineIndex]
let newLine = targetLine
if (targetLine.match(checkedMatch)) { if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') newLine = targetLine.replace(checkReplace, '[ ]')
} }
if (targetLine.match(uncheckedMatch)) { if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') newLine = targetLine.replace(uncheckReplace, '[x]')
} }
this.refs.code.setValue(lines.join('\n')) this.refs.code.setLineContent(lineIndex, newLine)
} }
} }
handleMouseMove (e) { handleMouseMove(e) {
if (this.state.isSliderFocused) { if (this.state.isSliderFocused) {
const rootRect = this.refs.root.getBoundingClientRect() const rootRect = this.refs.root.getBoundingClientRect()
const rootWidth = rootRect.width if (this.props.isStacking) {
const offset = rootRect.left const rootHeight = rootRect.height
let newCodeEditorWidthInPercent = (e.pageX - offset) / rootWidth * 100 const offset = rootRect.top
let newCodeEditorHeightInPercent =
((e.pageY - offset) / rootHeight) * 100
// limit minSize to 10%, maxSize to 90% // limit minSize to 10%, maxSize to 90%
if (newCodeEditorWidthInPercent <= 10) { if (newCodeEditorHeightInPercent <= 10) {
newCodeEditorWidthInPercent = 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
})
} }
if (newCodeEditorWidthInPercent >= 90) {
newCodeEditorWidthInPercent = 90
}
this.setState({
codeEditorWidthInPercent: newCodeEditorWidthInPercent
})
} }
} }
handleMouseUp (e) { handleMouseUp(e) {
e.preventDefault() e.preventDefault()
this.setState({ this.setState({
isSliderFocused: false isSliderFocused: false
}) })
} }
handleMouseDown (e) { handleMouseDown(e) {
e.preventDefault() e.preventDefault()
this.setState({ this.setState({
isSliderFocused: true isSliderFocused: true
}) })
} }
render () { render() {
const {config, value, storageKey, noteKey, linesHighlighted} = this.props const {
const storage = findStorage(storageKey) config,
value,
storageKey,
noteKey,
linesHighlighted,
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 (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 if (!(editorStyle.fontSize > 0 && editorStyle.fontSize < 132))
const previewStyle = {} editorIndentSize = 4
previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%' editorStyle.indentSize = editorIndentSize
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) 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 styleName='root' ref='root' <div
styleName='root'
ref='root'
onMouseMove={e => this.handleMouseMove(e)} onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}> onMouseUp={e => this.handleMouseUp(e)}
>
<CodeEditor <CodeEditor
styleName='codeEditor'
ref='code' ref='code'
width={this.state.codeEditorWidthInPercent + '%'} width={editorStyle.width}
height={editorStyle.height}
mode='Boost Flavored Markdown' 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={editorFontSize} fontSize={editorStyle.fontSize}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs} codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples} codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs} codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorIndentSize} indentSize={editorStyle.indentSize}
enableRulers={config.editor.enableRulers} enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers} rulers={config.editor.rulers}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
@@ -176,19 +279,27 @@ class MarkdownSplitEditor extends React.Component {
storageKey={storageKey} storageKey={storageKey}
noteKey={noteKey} noteKey={noteKey}
linesHighlighted={linesHighlighted} linesHighlighted={linesHighlighted}
onChange={(e) => this.handleOnChange(e)} onChange={e => this.handleOnChange(e)}
onScroll={this.handleScroll.bind(this)} onScroll={this.handleScroll.bind(this)}
spellCheck={config.editor.spellcheck} spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste} enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey} hotkey={config.hotkey}
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
/> enableMarkdownLint={config.editor.enableMarkdownLint}
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} > customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
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 styleName='slider-hitbox' />
</div> </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}
@@ -201,10 +312,10 @@ class MarkdownSplitEditor extends React.Component {
smartArrows={config.preview.smartArrows} smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks} breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
ref='preview' mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
tabInde='0' tabInde='0'
value={value} value={value}
onCheckboxClick={(e) => this.handleCheckboxClick(e)} onCheckboxClick={e => this.handleCheckboxClick(e)}
onScroll={this.handleScroll.bind(this)} onScroll={this.handleScroll.bind(this)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
@@ -212,7 +323,8 @@ class MarkdownSplitEditor extends React.Component {
customCSS={config.preview.customCSS} customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS} allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox} lineThroughCheckbox={config.preview.lineThroughCheckbox}
/> RTL={RTL}
/>
</div> </div>
) )
} }

View File

@@ -3,14 +3,36 @@
height 100% height 100%
font-size 30px font-size 30px
display flex display flex
flex-wrap wrap
.slider .slider
absolute top bottom absolute top bottom
top -2px top -2px
width 0 width 0
z-index 0 z-index 0
border-left 1px solid $ui-borderColor
.slider-hitbox .slider-hitbox
absolute top bottom left right absolute top bottom left right
width 7px width 7px
left -3px left -3px
z-index 10 z-index 10
cursor col-resize cursor col-resize
.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,12 +3,10 @@ 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 = ({ const ModalEscButton = ({ handleEscButtonClick }) => (
handleEscButtonClick
}) => (
<button styleName='escButton' onClick={handleEscButtonClick}> <button styleName='escButton' onClick={handleEscButtonClick}>
<div styleName='esc-mark'>×</div> <div styleName='esc-mark'>×</div>
<div styleName='esc-text'>esc</div> <div>esc</div>
</button> </button>
) )

View File

@@ -1,24 +1,23 @@
/** /**
* @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' <button styleName='navToggle' onClick={e => handleToggleButtonClick(e)}>
onClick={(e) => handleToggleButtonClick(e)} {isFolded ? (
> <i className='fa fa-angle-double-right fa-2x' />
{isFolded ) : (
? <i className='fa fa-angle-double-right' /> <i className='fa fa-angle-double-left fa-2x' />
: <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 32px line-height 100%
padding 0 padding 0
&:hover &:hover
border: 1px solid #1EC38B; border: 1px solid #1EC38B;
@@ -17,10 +17,16 @@
body[data-theme="white"] body[data-theme="white"]
navWhiteButtonColor() navWhiteButtonColor()
body[data-theme="dark"] apply-theme(theme)
.navToggle body[data-theme={theme}]
&:hover .navToggle:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%) background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%)
border 1px solid get-theme-var(theme, 'button--active-backgroundColor')
transition 0.15s transition 0.15s
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
for theme in 'dark' 'dracula' 'solarized-dark'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -3,7 +3,9 @@
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { isArray } from 'lodash' import { isArray, sortBy } from 'lodash'
import invertColor from 'invert-color'
import 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'
@@ -13,29 +15,46 @@ import i18n from 'browser/lib/i18n'
/** /**
* @description Tag element component. * @description Tag element component.
* @param {string} tagName * @param {string} tagName
* @param {string} color
* @return {React.Component} * @return {React.Component}
*/ */
const TagElement = ({ tagName }) => ( const TagElement = ({ tagName, color }) => {
<span styleName='item-bottom-tagList-item' key={tagName}> const style = {}
#{tagName} if (color) {
</span> style.backgroundColor = color
) style.color = invertColor(color, {
black: '#222',
white: '#f1f1f1',
threshold: 0.3
})
}
return (
<span styleName='item-bottom-tagList-item' key={tagName} style={style}>
#{tagName}
</span>
)
}
/** /**
* @description Tag element list component. * @description Tag element list component.
* @param {Array|null} tags * @param {Array|null} tags
* @param {boolean} showTagsAlphabetically * @param {boolean} showTagsAlphabetically
* @param {Object} coloredTags
* @return {React.Component} * @return {React.Component}
*/ */
const TagElementList = (tags, showTagsAlphabetically) => { const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
if (!isArray(tags)) { if (!isArray(tags)) {
return [] return []
} }
if (showTagsAlphabetically) { if (showTagsAlphabetically) {
return _.sortBy(tags).map(tag => TagElement({ tagName: tag })) return sortBy(tags).map(tag =>
TagElement({ tagName: tag, color: coloredTags[tag] })
)
} else { } else {
return tags.map(tag => TagElement({ tagName: tag })) return tags.map(tag =>
TagElement({ tagName: tag, color: coloredTags[tag] })
)
} }
} }
@@ -46,6 +65,7 @@ const TagElementList = (tags, showTagsAlphabetically) => {
* @param {Function} handleNoteClick * @param {Function} handleNoteClick
* @param {Function} handleNoteContextMenu * @param {Function} handleNoteContextMenu
* @param {Function} handleDragStart * @param {Function} handleDragStart
* @param {Object} coloredTags
* @param {string} dateDisplay * @param {string} dateDisplay
*/ */
const NoteItem = ({ const NoteItem = ({
@@ -59,7 +79,8 @@ const NoteItem = ({
storageName, storageName,
folderName, folderName,
viewType, viewType,
showTagsAlphabetically showTagsAlphabetically,
coloredTags
}) => ( }) => (
<div <div
styleName={isActive ? 'item--active' : 'item'} styleName={isActive ? 'item--active' : 'item'}
@@ -70,13 +91,17 @@ const NoteItem = ({
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 ? (
? note.title <Emoji text={note.title} />
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>} ) : (
<span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
)}
</div> </div>
<div styleName='item-middle'> <div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div> <div styleName='item-middle-time'>{dateDisplay}</div>
@@ -85,7 +110,9 @@ const NoteItem = ({
title={ title={
viewType === 'ALL' viewType === 'ALL'
? storageName ? storageName
: viewType === 'STORAGE' ? folderName : null : viewType === 'STORAGE'
? folderName
: null
} }
styleName='item-middle-app-meta-label' styleName='item-middle-app-meta-label'
> >
@@ -96,28 +123,36 @@ const NoteItem = ({
</div> </div>
<div styleName='item-bottom'> <div styleName='item-bottom'>
<div styleName='item-bottom-tagList'> <div styleName='item-bottom-tagList'>
{note.tags.length > 0 {note.tags.length > 0 ? (
? TagElementList(note.tags, showTagsAlphabetically) TagElementList(note.tags, showTagsAlphabetically, coloredTags)
: <span ) : (
<span
style={{ fontStyle: 'italic', opacity: 0.5 }} style={{ fontStyle: 'italic', opacity: 0.5 }}
styleName='item-bottom-tagList-empty' styleName='item-bottom-tagList-empty'
> >
{i18n.__('No tags')} {i18n.__('No tags')}
</span>} </span>
)}
</div> </div>
<div> <div>
{note.isStarred {note.isStarred ? (
? <img <img
styleName='item-star' styleName='item-star'
src='../resources/icon/icon-starred.svg' src='../resources/icon/icon-starred.svg'
/> />
: ''} ) : (
{note.isPinned && !pathname.match(/\/starred|\/trash/) ''
? <i styleName='item-pin' className='fa fa-thumb-tack' /> )}
: ''} {note.isPinned && !pathname.match(/\/starred|\/trash/) ? (
{note.type === 'MARKDOWN_NOTE' <i styleName='item-pin' className='fa fa-thumb-tack' />
? <TodoProcess todoStatus={getTodoStatus(note.content)} /> ) : (
: ''} ''
)}
{note.type === 'MARKDOWN_NOTE' ? (
<TodoProcess todoStatus={getTodoStatus(note.content)} />
) : (
''
)}
</div> </div>
</div> </div>
</div> </div>
@@ -127,6 +162,7 @@ const NoteItem = ({
NoteItem.propTypes = { NoteItem.propTypes = {
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
dateDisplay: PropTypes.string.isRequired, dateDisplay: PropTypes.string.isRequired,
coloredTags: PropTypes.object,
note: PropTypes.shape({ note: PropTypes.shape({
storage: PropTypes.string.isRequired, storage: PropTypes.string.isRequired,
key: PropTypes.string.isRequired, key: PropTypes.string.isRequired,
@@ -135,15 +171,14 @@ NoteItem.propTypes = {
tags: PropTypes.array, tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired, isStarred: PropTypes.bool.isRequired,
isTrashed: PropTypes.bool.isRequired, isTrashed: PropTypes.bool.isRequired,
blog: { blog: PropTypes.shape({
blogLink: PropTypes.string, blogLink: PropTypes.string,
blogId: PropTypes.number blogId: PropTypes.number
} })
}), }),
handleNoteClick: PropTypes.func.isRequired, handleNoteClick: PropTypes.func.isRequired,
handleNoteContextMenu: PropTypes.func.isRequired, handleNoteContextMenu: PropTypes.func.isRequired,
handleDragStart: PropTypes.func.isRequired, handleDragStart: PropTypes.func.isRequired
handleDragEnd: PropTypes.func.isRequired
} }
export default CSSModules(NoteItem, styles) export default CSSModules(NoteItem, styles)

View File

@@ -194,7 +194,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(#fff, 20%) background-color alpha($ui-dark-tagList-backgroundColor, 20%)
color $ui-dark-text-color color $ui-dark-text-color
&:active &:active
transition 0.15s transition 0.15s
@@ -207,7 +207,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(white, 10%) background-color alpha($ui-dark-tagList-backgroundColor, 10%)
color $ui-dark-text-color color $ui-dark-text-color
.item-wrapper .item-wrapper
@@ -223,13 +223,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(white, 10%) background-color alpha($ui-dark-tagList-backgroundColor, 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 #c0392b color $ui-dark-button--hover-color
.item-bottom-tagList-item .item-bottom-tagList-item
background-color alpha(#fff, 20%) background-color alpha($ui-dark-tagList-backgroundColor, 20%)
.item-title .item-title
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -322,148 +322,82 @@ body[data-theme="solarized-dark"]
color $ui-inactive-text-color color $ui-inactive-text-color
vertical-align middle vertical-align middle
body[data-theme="monokai"] apply-theme(theme)
.root body[data-theme={theme}]
border-color $ui-monokai-borderColor .root
background-color $ui-monokai-noteList-backgroundColor border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteList-backgroundColor')
.item .item
border-color $ui-monokai-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-monokai-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
&:hover &:hover
transition 0.15s
// background-color alpha($ui-monokai-noteList-backgroundColor, 20%)
color $ui-monokai-text-color
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s transition 0.15s
color $ui-monokai-text-color // background-color alpha(get-theme-var(theme, 'noteList-backgroundColor'), 20%)
.item-bottom-tagList-item 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 transition 0.15s
background-color alpha($ui-monokai-noteList-backgroundColor, 20%) background-color get-theme-var(theme, 'noteList-backgroundColor')
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
&:active .item-title
transition 0.15s .item-title-icon
background-color $ui-monokai-noteList-backgroundColor .item-bottom-time
color $ui-monokai-text-color transition 0.15s
.item-title color get-theme-var(theme, 'text-color')
.item-title-icon .item-bottom-tagList-item
.item-bottom-time transition 0.15s
transition 0.15s background-color alpha(get-theme-var(theme, 'noteList-backgroundColor'), 10%)
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-monokai-noteList-backgroundColor, 10%)
color $ui-monokai-text-color
.item-wrapper
border-color alpha($ui-monokai-button-backgroundColor, 60%)
.item--active
border-color $ui-monokai-borderColor
background-color $ui-monokai-button-backgroundColor
.item-wrapper .item-wrapper
border-color transparent border-color alpha(get-theme-var(theme, 'button-backgroundColor'), 60%)
.item-title
.item-title-icon
.item-bottom-time
color $ui-monokai-active-color
.item-bottom-tagList-item
background-color alpha(white, 10%)
color $ui-monokai-text-color
&:hover
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%)
color #f92672
.item-bottom-tagList-item
background-color alpha(#fff, 20%)
.item-title .item--active
color $ui-inactive-text-color border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'button-backgroundColor')
.item-title-icon .item-wrapper
color $ui-inactive-text-color border-color transparent
.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
body[data-theme="dracula"]
.root
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
.item
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
&:hover
transition 0.15s
// background-color alpha($ui-dracula-noteList-backgroundColor, 20%)
color $ui-dracula-text-color
.item-title .item-title
.item-title-icon .item-title-icon
.item-bottom-time .item-bottom-time
transition 0.15s color get-theme-var(theme, 'active-color')
color $ui-dracula-text-color
.item-bottom-tagList-item .item-bottom-tagList-item
transition 0.15s background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 10%)
background-color alpha($ui-dracula-noteList-backgroundColor, 20%) color get-theme-var(theme, 'text-color')
color $ui-dracula-text-color &:hover
&:active // background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%)
transition 0.15s color get-theme-var(theme, 'button--hover-color')
background-color $ui-dracula-noteList-backgroundColor .item-bottom-tagList-item
color $ui-dracula-text-color background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 20%)
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-dracula-noteList-backgroundColor, 10%)
color $ui-dracula-text-color
.item-wrapper
border-color alpha($ui-dracula-button-backgroundColor, 60%)
.item--active
border-color $ui-dracula-borderColor
background-color $ui-dracula-button-backgroundColor
.item-wrapper
border-color transparent
.item-title .item-title
color $ui-inactive-text-color
.item-title-icon .item-title-icon
.item-bottom-time color $ui-inactive-text-color
color $ui-dracula-active-color
.item-title-empty
color $ui-inactive-text-color
.item-bottom-tagList-item .item-bottom-tagList-item
background-color alpha(#f8f8f2, 10%) background-color alpha($ui-dark-button--active-backgroundColor, 40%)
color $ui-dracula-text-color color $ui-inactive-text-color
&:hover
// background-color alpha($ui-dracula-button--active-backgroundColor, 60%)
color #ff79c6
.item-bottom-tagList-item
background-color alpha(#f8f8f2, 20%)
.item-title .item-bottom-tagList-empty
color $ui-inactive-text-color color $ui-inactive-text-color
vertical-align middle
.item-title-icon for theme in 'dracula'
color $ui-inactive-text-color apply-theme(theme)
.item-title-empty for theme in $themes
color $ui-inactive-text-color apply-theme(theme)
.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

View File

@@ -25,10 +25,8 @@ const NoteItemSimple = ({
pathname, pathname,
storage storage
}) => ( }) => (
<div styleName={isActive <div
? 'item-simple--active' styleName={isActive ? 'item-simple--active' : 'item-simple'}
: 'item-simple'
}
key={note.key} key={note.key}
onClick={e => handleNoteClick(e, note.key)} onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)} onContextMenu={e => handleNoteContextMenu(e, note.key)}
@@ -36,23 +34,29 @@ const NoteItemSimple = ({
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
{note.isPinned && !pathname.match(/\/starred|\/trash/) styleName='item-simple-title-icon'
? <i styleName='item-pin' className='fa fa-thumb-tack' /> className='fa fa-fw fa-file-text-o'
: '' />
} )}
{note.title.trim().length > 0 {note.isPinned && !pathname.match(/\/starred|\/trash/) ? (
? note.title <i styleName='item-pin' className='fa fa-thumb-tack' />
: <span styleName='item-simple-title-empty'>{i18n.__('Empty note')}</span> ) : (
} ''
{isAllNotesView && <div styleName='item-simple-right'> )}
<span styleName='item-simple-right-storageName'> {note.title.trim().length > 0 ? (
{storage.name} note.title
</span> ) : (
</div>} <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

@@ -223,130 +223,73 @@ body[data-theme="solarized-dark"]
padding-left 4px padding-left 4px
opacity 0.4 opacity 0.4
body[data-theme="monokai"] apply-theme(theme)
.root body[data-theme={theme}]
border-color $ui-monokai-borderColor .root
background-color $ui-monokai-noteList-backgroundColor border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteList-backgroundColor')
.item-simple .item-simple
border-color $ui-monokai-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-monokai-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
&:hover &:hover
transition 0.15s transition 0.15s
background-color alpha($ui-monokai-button-backgroundColor, 60%) background-color alpha(get-theme-var(theme, 'button-backgroundColor'), 60%)
color $ui-monokai-text-color 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
.item-simple-title-empty .item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s color get-theme-var(theme, 'text-color')
color $ui-monokai-text-color
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
transition 0.15s background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 10%)
background-color alpha(#fff, 20%) color get-theme-var(theme, 'text-color')
color $ui-monokai-text-color &:hover
&:active // background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%)
transition 0.15s color #c0392b
background-color $ui-monokai-button--active-backgroundColor .item-simple-bottom-tagList-item
color $ui-monokai-text-color background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 20%)
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-monokai-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(white, 10%)
color $ui-monokai-text-color
.item-simple--active
border-color $ui-monokai-borderColor
background-color $ui-monokai-button--active-backgroundColor
.item-simple-wrapper
border-color transparent
.item-simple-title .item-simple-title
.item-simple-title-empty color $ui-dark-text-color
.item-simple-title-icon border-bottom $ui-dark-borderColor
.item-simple-bottom-time .item-simple-right
color $ui-monokai-text-color float right
.item-simple-bottom-tagList-item .item-simple-right-storageName
background-color alpha(white, 10%) padding-left 4px
color $ui-monokai-text-color opacity 0.4
&:hover
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
color #c0392b
.item-simple-bottom-tagList-item
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
body[data-theme="dracula"] for theme in 'dracula'
.root apply-theme(theme)
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
.item-simple for theme in $themes
border-color $ui-dracula-borderColor apply-theme(theme)
background-color $ui-dracula-noteList-backgroundColor
&:hover
transition 0.15s
background-color alpha($ui-dracula-button-backgroundColor, 60%)
color $ui-dracula-text-color
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(#f8f8f2, 20%)
color $ui-dracula-text-color
&:active
transition 0.15s
background-color $ui-dracula-button--active-backgroundColor
color $ui-dracula-text-color
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(#f8f8f2, 10%)
color $ui-dracula-text-color
.item-simple--active
border-color $ui-dracula-borderColor
background-color $ui-dracula-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 $ui-dracula-text-color
.item-simple-bottom-tagList-item
background-color alpha(#f8f8f2, 10%)
color $ui-dracula-text-color
&:hover
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
color #c0392b
.item-simple-bottom-tagList-item
background-color alpha(#f8f8f2, 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

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,38 +14,46 @@ class RealtimeNotification extends React.Component {
} }
} }
componentDidMount () { componentDidMount() {
this.fetchNotifications() this.fetchNotifications()
} }
fetchNotifications () { fetchNotifications() {
const notificationsUrl = 'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json' const notificationsUrl =
'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 = notifications.length > 0 const link =
? <a styleName='notification-link' href={notifications[0].linkUrl} notifications.length > 0 ? (
onClick={(e) => this.handleLinkClick(e)} <a
> styleName='notification-link'
Info: {notifications[0].text} href={notifications[0].linkUrl}
</a> onClick={e => this.handleLinkClick(e)}
: '' >
Info: {notifications[0].text}
</a>
) : (
''
)
return ( return (
<div styleName='notification-area' style={this.props.style}>{link}</div> <div styleName='notification-area' style={this.props.style}>
{link}
</div>
) )
} }
} }

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
.menu .menu
margin-bottom 30px margin-bottom 20px
.menu-button .menu-button
navButtonColor() navButtonColor()
@@ -180,129 +180,51 @@ 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')
body[data-theme="solarized-dark"] .menu-button--active
.menu-button color get-theme-var(theme, 'text-color')
&:active background-color get-theme-var(theme, 'button-backgroundColor')
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 $ui-solarized-dark-text-color 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-label
color get-theme-var(theme, 'text-color')
.menu-button-star--active .menu-button-star--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')
.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 $ui-solarized-dark-text-color 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-label
color get-theme-var(theme, 'text-color')
.menu-button-trash--active .menu-button-trash--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')
.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 $ui-solarized-dark-text-color 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-label
color get-theme-var(theme, 'text-color')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.menu-button apply-theme(theme)
&:active
background-color $ui-monokai-noteList-backgroundColor
color $ui-monokai-text-color
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
.menu-button--active for theme in $themes
color $ui-monokai-text-color apply-theme(theme)
background-color $ui-monokai-button-backgroundColor
.menu-button-label
color $ui-monokai-text-color
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
.menu-button-label
color $ui-monokai-text-color
.menu-button-star--active
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
.menu-button-label
color $ui-monokai-text-color
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
.menu-button-label
color $ui-monokai-text-color
.menu-button-trash--active
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
.menu-button-label
color $ui-monokai-text-color
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
.menu-button-label
color $ui-monokai-text-color
body[data-theme="dracula"]
.menu-button
&:active
background-color $ui-dracula-noteList-backgroundColor
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.menu-button-label
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button-label
color $ui-dracula-text-color
.menu-button-star--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.menu-button-label
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button-label
color $ui-dracula-text-color
.menu-button-trash--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.menu-button-label
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button-label
color $ui-dracula-text-color

View File

@@ -5,7 +5,7 @@ import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n' 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 +14,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,34 +22,34 @@ 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: i18n.__('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()
@@ -63,84 +63,87 @@ class SnippetTab extends React.Component {
} }
} }
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 <div styleName={isActive ? 'root--active' : 'root'}>
? 'root--active' {!this.state.isRenaming ? (
: 'root' <button
} styleName='button'
> onClick={e => this.handleClick(e)}
{!this.state.isRenaming onDoubleClick={e => this.handleRenameClick(e)}
? <button styleName='button' onContextMenu={e => this.handleContextMenu(e)}
onClick={(e) => this.handleClick(e)} onDragStart={e => this.handleDragStart(e)}
onDoubleClick={(e) => this.handleRenameClick(e)} onDrop={e => this.handleDrop(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'> ) : (
{i18n.__('Unnamed')} <span>{i18n.__('Unnamed')}</span>
</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 styleName='deleteButton' <button
onClick={(e) => this.handleDeleteButtonClick(e)} styleName='deleteButton'
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

@@ -61,7 +61,7 @@
width 100% width 100%
outline none outline none
body[data-theme="default"], body[data-theme="white"] body[data-theme="default"], body[data-theme="white"]
.root--active .root--active
&:hover &:hover
background-color alpha($ui-button--active-backgroundColor, 60%) background-color alpha($ui-button--active-backgroundColor, 60%)
@@ -100,103 +100,43 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
transition 0.15s transition 0.15s
body[data-theme="solarized-dark"] apply-theme(theme)
.root body[data-theme={theme}]
border-color $ui-solarized-dark-borderColor .root
&:hover border-color get-theme-var(theme, 'borderColor')
background-color $ui-solarized-dark-noteDetail-backgroundColor &:hover
transition 0.15s 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
color get-theme-var(theme, 'active-color')
background-color get-theme-var(theme, 'button-backgroundColor')
border-color get-theme-var(theme, 'borderColor')
.deleteButton .deleteButton
color $ui-solarized-dark-button--active-color color get-theme-var(theme, 'text-color')
transition 0.15s
.button .button
color $ui-solarized-dark-button--active-color color get-theme-var(theme, 'active-color')
transition 0.15s
.root--active
color $ui-solarized-dark-button--active-color
background-color $ui-solarized-dark-button-backgroundColor
border-color $ui-solarized-dark-borderColor
.deleteButton
color $ui-solarized-dark-button--active-color
.button .button
color $ui-solarized-dark-button--active-color border none
color $ui-inactive-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
.button .input
border none background-color get-theme-var(theme, 'noteDetail-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
.input
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-button--active-color
transition 0.15s
body[data-theme="monokai"]
.root
border-color $ui-monokai-borderColor
&:hover
background-color $ui-monokai-noteDetail-backgroundColor
transition 0.15s transition 0.15s
.deleteButton
color $ui-monokai-text-color
transition 0.15s
.button
color $ui-monokai-text-color
transition 0.15s
.root--active for theme in 'solarized-dark' 'dracula'
color $ui-monokai-active-color apply-theme(theme)
background-color $ui-monokai-button-backgroundColor
border-color $ui-monokai-borderColor
.deleteButton
color $ui-monokai-text-color
.button
color $ui-monokai-active-color
.button
border none
color $ui-inactive-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
.input for theme in $themes
background-color $ui-monokai-noteDetail-backgroundColor apply-theme(theme)
color $ui-monokai-text-color
transition 0.15s
body[data-theme="dracula"]
.root
border-color $ui-dracula-borderColor
&:hover
background-color $ui-dracula-noteDetail-backgroundColor
transition 0.15s
.deleteButton
color $ui-dracula-text-color
transition 0.15s
.button
color $ui-dracula-text-color
transition 0.15s
.root--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
border-color $ui-dracula-borderColor
.deleteButton
color $ui-dracula-text-color
.button
color $ui-dracula-active-color
.button
border none
color $ui-inactive-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
.input
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color

View File

@@ -21,7 +21,9 @@ const FolderIcon = ({ className, color, isActive }) => {
/** /**
* @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,7 +37,9 @@ const FolderIcon = ({ className, color, isActive }) => {
const StorageItem = ({ const StorageItem = ({
styles, styles,
isActive, isActive,
tooltipRef,
handleButtonClick, handleButtonClick,
handleMouseEnter,
handleContextMenu, handleContextMenu,
folderName, folderName,
folderColor, folderColor,
@@ -49,13 +53,15 @@ const StorageItem = ({
<button <button
styleName={isActive ? 'folderList-item--active' : 'folderList-item'} styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
onClick={handleButtonClick} onClick={handleButtonClick}
onMouseEnter={handleMouseEnter}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onDrop={handleDrop} onDrop={handleDrop}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
> >
{!isFolded && {!isFolded && (
<DraggableIcon className={styles['folderList-item-reorder']} />} <DraggableIcon className={styles['folderList-item-reorder']} />
)}
<span <span
styleName={ styleName={
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name' isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
@@ -70,18 +76,23 @@ const StorageItem = ({
? _.truncate(folderName, { length: 1, omission: '' }) ? _.truncate(folderName, { length: 1, omission: '' })
: folderName} : folderName}
</span> </span>
{!isFolded && {!isFolded && _.isNumber(noteCount) && (
_.isNumber(noteCount) && <span styleName='folderList-item-noteCount'>{noteCount}</span>
<span styleName='folderList-item-noteCount'>{noteCount}</span>} )}
{isFolded && {isFolded && (
<span styleName='folderList-item-tooltip'>{folderName}</span>} <span styleName='folderList-item-tooltip' ref={tooltipRef}>
{folderName}
</span>
)}
</button> </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

@@ -60,6 +60,7 @@
border-bottom-right-radius 2px border-bottom-right-radius 2px
height 34px height 34px
line-height 32px line-height 32px
transition-property opacity
.folderList-item:hover, .folderList-item--active:hover .folderList-item:hover, .folderList-item--active:hover
.folderList-item-tooltip .folderList-item-tooltip
@@ -120,59 +121,28 @@ 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%)
body[data-theme="solarized-dark"] apply-theme(theme)
.folderList-item body[data-theme={theme}]
&:hover .folderList-item
background-color $ui-solarized-dark-button-backgroundColor &:hover
color $ui-solarized-dark-text-color background-color get-theme-var(theme, 'button-backgroundColor')
&:active color get-theme-var(theme, 'text-color')
color $ui-solarized-dark-text-color &:active
background-color $ui-solarized-dark-button-backgroundColor color get-theme-var(theme, 'text-color')
background-color get-theme-var(theme, 'button-backgroundColor')
.folderList-item--active .folderList-item--active
@extend .folderList-item @extend .folderList-item
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')
&:active &:active
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
&:hover &:hover
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')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.folderList-item apply-theme(theme)
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
&:active
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
.folderList-item--active for theme in $themes
@extend .folderList-item apply-theme(theme)
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
&:active
background-color $ui-monokai-button-backgroundColor
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
body[data-theme="dracula"]
.folderList-item
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
&:active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.folderList-item--active
@extend .folderList-item
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
&:active
background-color $ui-dracula-button-backgroundColor
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor

View File

@@ -1,18 +1,20 @@
/** /**
* @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} storageList
*/ */
const StorageList = ({storageList, isFolded}) => ( const StorageList = ({ storageList, isFolded }) => (
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}> <div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
{storageList.length > 0 ? storageList : ( {storageList.length > 0 ? (
storageList
) : (
<div styleName='storageList-empty'>No storage mount.</div> <div styleName='storageList-empty'>No storage mount.</div>
)} )}
</div> </div>

View File

@@ -1,28 +1,58 @@
/** /**
* @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 {Function} handleClickNarrowToTag
* @param {bool} isActive * @param {boolean} isActive
* @param {bool} isRelated * @param {boolean} isRelated
*/ * @param {string} bgColor tab backgroundColor
*/
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => ( const TagListItem = ({
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}> name,
{isRelated handleClickTagListItem,
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}> handleClickNarrowToTag,
handleContextMenu,
isActive,
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'} /> <i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
</button> </button>
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} /> ) : (
} <div
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}> 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'> <span styleName='tagList-item-name'>
{`# ${name}`} {`# ${name}`}
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span> <span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
@@ -33,7 +63,8 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, hand
TagListItem.propTypes = { TagListItem.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
handleClickTagListItem: PropTypes.func.isRequired handleClickTagListItem: PropTypes.func.isRequired,
color: PropTypes.string
} }
export default CSSModules(TagListItem, styles) export default CSSModules(TagListItem, styles)

View File

@@ -71,6 +71,11 @@
padding-right 15px padding-right 15px
font-size 13px font-size 13px
.tagList-item-color
height 26px
width 3px
display inline-block
body[data-theme="white"] body[data-theme="white"]
.tagList-item .tagList-item
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -89,23 +94,30 @@ body[data-theme="white"]
.tagList-item-count .tagList-item-count
color $ui-text-color color $ui-text-color
body[data-theme="dark"] apply-theme(theme)
.tagList-item body[data-theme={theme}]
color $ui-dark-inactive-text-color .tagList-item
&:hover color get-theme-var(theme, 'inactive-text-color')
color $ui-dark-text-color &:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%) color get-theme-var(theme, 'text-color')
&:active background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%)
color $ui-dark-text-color &:active
background-color $ui-dark-button--active-backgroundColor color get-theme-var(theme, 'text-color')
background-color get-theme-var(theme, 'button--active-backgroundColor')
.tagList-item-active .tagList-item-active
background-color $ui-dark-button--active-backgroundColor background-color get-theme-var(theme, 'button--active-backgroundColor')
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
&:active &:active
background-color alpha($ui-dark-button--active-backgroundColor, 50%) background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 50%)
&:hover &:hover
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
background-color alpha($ui-dark-button--active-backgroundColor, 50%) background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 50%)
.tagList-item-count .tagList-item-count
color $ui-dark-button--active-color 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,17 +11,20 @@ import styles from './TodoListPercentage.styl'
* @param {number} percentageOfTodo * @param {number} percentageOfTodo
*/ */
const TodoListPercentage = ({ const TodoListPercentage = ({ percentageOfTodo, onClearCheckboxClick }) => (
percentageOfTodo, onClearCheckboxClick <div
}) => ( styleName='percentageBar'
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}> 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'> <div styleName='todoClear'>
<p styleName='todoClearText' onClick={(e) => onClearCheckboxClick(e)}>clear</p> <p styleName='todoClearText' onClick={e => onClearCheckboxClick(e)}>
clear
</p>
</div> </div>
</div> </div>
) )

View File

@@ -54,7 +54,7 @@ body[data-theme="dark"]
.percentageText .percentageText
color $ui-dark-text-color color $ui-dark-text-color
.todoClearText .todoClearText
color $ui-dark-text-color color $ui-dark-text-color
@@ -71,25 +71,19 @@ body[data-theme="solarized-dark"]
.todoClearText .todoClearText
color #fdf6e3 color #fdf6e3
body[data-theme="monokai"] apply-theme(theme)
.percentageBar body[data-theme={theme}]
background-color: $ui-monokai-borderColor .percentageBar
background-color: get-theme-var(theme, 'borderColor')
.progressBar .progressBar
background-color $ui-monokai-active-color background-color get-theme-var(theme, 'active-color')
.percentageText .percentageText
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
body[data-theme="dracula"] for theme in 'dracula'
.percentageBar apply-theme(theme)
background-color $ui-dracula-borderColor
.progressBar for theme in $themes
background-color: $ui-dracula-active-color apply-theme(theme)
.percentageText
color $ui-dracula-text-color
.percentageText
color $ui-dracula-text-color

View File

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

View File

@@ -61,6 +61,8 @@ body
// allow inline line breaks // allow inline line breaks
.katex .katex
white-space initial white-space initial
.katex .katex-html
display inline-flex
.katex .mfrac>.vlist>span:nth-child(2) .katex .mfrac>.vlist>span:nth-child(2)
top 0 !important top 0 !important
.katex-error .katex-error
@@ -122,40 +124,34 @@ 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
padding-bottom 0.3em line-height 1.2
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
padding-bottom 0.3em line-height 1.225
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.43em line-height 1.43
margin 1em 0 0.66em
h4 h4
font-size 1.25em font-size 1.25em
line-height 1.4em line-height 1.4
margin 1em 0 0.8em
h5 h5
font-size 1em font-size 1em
line-height 1.4em line-height 1.1
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
@@ -163,6 +159,7 @@ p
white-space pre-line white-space pre-line
word-wrap break-word word-wrap break-word
img img
cursor zoom-in
max-width 100% max-width 100%
strong, b strong, b
font-weight bold font-weight bold
@@ -360,7 +357,10 @@ admonition_types = {
danger: {color: #c2185b, icon: "block"}, danger: {color: #c2185b, icon: "block"},
caution: {color: #ffa726, icon: "warning"}, caution: {color: #ffa726, icon: "warning"},
error: {color: #d32f2f, icon: "error_outline"}, error: {color: #d32f2f, icon: "error_outline"},
attention: {color: #455a64, icon: "priority_high"} question: {color: #64dd17, icon: "help_outline"},
quote: {color: #9e9e9e, icon: "format_quote"},
abstract: {color: #00b0ff, icon: "subject"},
attention: {color: #455a64, icon: "priority_high"},
} }
for name, val in admonition_types for name, val in admonition_types
@@ -421,6 +421,9 @@ pre.fence
canvas, svg canvas, svg
max-width 100% !important max-width 100% !important
svg[ratio]
width 100%
.gallery .gallery
width 100% width 100%
height 50vh height 50vh
@@ -441,6 +444,44 @@ pre.fence
color $ui-text-color color $ui-text-color
background-color $ui-tag-backgroundColor 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%)
@@ -508,137 +549,63 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
background-color $ui-dark-tag-backgroundColor background-color $ui-dark-tag-backgroundColor
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor .markdownIt-TOC-wrapper
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%) &,
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven &:before
themeSolarizedDarkTableBorder = themeDarkBorder background-color darken(themeDarkBackground, 5%)
color themeDarkText
body[data-theme="solarized-dark"] apply-theme(theme)
color $ui-solarized-dark-text-color body[data-theme={theme}]
border-color themeDarkBorder color get-theme-var(theme, 'text-color')
background-color $ui-solarized-dark-noteDetail-backgroundColor
table
thead
tr
background-color themeSolarizedDarkTableHead
th
border-color themeSolarizedDarkTableBorder
&:last-child
border-right solid 1px themeSolarizedDarkTableBorder
tbody
tr:nth-child(2n + 1)
background-color themeSolarizedDarkTableOdd
tr:nth-child(2n)
background-color themeSolarizedDarkTableEven
td
border-color themeSolarizedDarkTableBorder
&:last-child
border-right solid 1px themeSolarizedDarkTableBorder
dl
border-color themeDarkBorder border-color themeDarkBorder
background-color themeSolarizedDarkTableHead background-color get-theme-var(theme, 'noteDetail-backgroundColor')
dt table
border-color themeDarkBorder thead
dd tr
border-color themeDarkBorder background-color get-theme-var(theme, 'table-head-backgroundColor')
background-color $ui-solarized-dark-noteDetail-backgroundColor th
border-color get-theme-var(theme, 'table-borderColor')
&:last-child
border-right solid 1px get-theme-var(theme, 'table-borderColor')
tbody
tr:nth-child(2n + 1)
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')
pre.fence dl
.gallery border-color themeDarkBorder
.carousel-main, .carousel-footer background-color get-theme-var(theme, 'table-head-backgroundColor')
background-color $ui-solarized-dark-noteDetail-backgroundColor dt
.prev, .next border-color themeDarkBorder
color $ui-solarized-dark-button--active-color dd
background-color $ui-solarized-dark-button-backgroundColor border-color themeDarkBorder
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor pre.fence
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%) .gallery
themeMonokaiTableHead = themeMonokaiTableEven .carousel-main, .carousel-footer
themeMonokaiTableBorder = themeDarkBorder 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')
body[data-theme="monokai"] .markdownIt-TOC-wrapper
color $ui-monokai-text-color &,
border-color themeDarkBorder &:before
background-color $ui-monokai-noteDetail-backgroundColor background-color darken(get-theme-var(theme, 'noteDetail-backgroundColor'), 15%)
table color themeDarkText
thead
tr
background-color themeMonokaiTableHead
th
border-color themeMonokaiTableBorder
&:last-child
border-right solid 1px themeMonokaiTableBorder
tbody
tr:nth-child(2n + 1)
background-color themeMonokaiTableOdd
tr:nth-child(2n)
background-color themeMonokaiTableEven
td
border-color themeMonokaiTableBorder
&:last-child
border-right solid 1px themeMonokaiTableBorder
kbd
background-color themeDarkBackground
dl for theme in 'solarized-dark' 'dracula'
border-color themeDarkBorder apply-theme(theme)
background-color themeMonokaiTableHead
dt
border-color themeDarkBorder
dd
border-color themeDarkBorder
background-color $ui-monokai-noteDetail-backgroundColor
pre.fence for theme in $themes
.gallery apply-theme(theme)
.carousel-main, .carousel-footer
background-color $ui-monokai-noteDetail-backgroundColor
.prev, .next
color $ui-monokai-button--active-color
background-color $ui-monokai-button-backgroundColor
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
themeDraculaTableHead = themeDraculaTableEven
themeDraculaTableBorder = themeDarkBorder
body[data-theme="dracula"]
color $ui-dracula-text-color
border-color themeDarkBorder
background-color $ui-dracula-noteDetail-backgroundColor
table
thead
tr
background-color themeDraculaTableHead
th
border-color themeDraculaTableBorder
&:last-child
border-right solid 1px themeDraculaTableBorder
tbody
tr:nth-child(2n + 1)
background-color themeDraculaTableOdd
tr:nth-child(2n)
background-color themeDraculaTableEven
td
border-color themeDraculaTableBorder
&:last-child
border-right solid 1px themeDraculaTableBorder
kbd
background-color themeDarkBackground
dl
border-color themeDarkBorder
background-color themeDraculaTableHead
dt
border-color themeDarkBorder
dd
border-color themeDarkBorder
background-color $ui-dracula-noteDetail-backgroundColor
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color $ui-dracula-noteDetail-backgroundColor
.prev, .next
color $ui-dracula-button--active-color
background-color $ui-dracula-button-backgroundColor

View File

@@ -1,4 +1,5 @@
import mermaidAPI from 'mermaid' import mermaidAPI from 'mermaid'
import uiThemes from 'browser/lib/ui-themes'
// fixes bad styling in the mermaid dark theme // fixes bad styling in the mermaid dark theme
const darkThemeStyling = ` const darkThemeStyling = `
@@ -6,11 +7,11 @@ const darkThemeStyling = `
fill: white; fill: white;
}` }`
function getRandomInt (min, max) { function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min return Math.floor(Math.random() * (max - min)) + min
} }
function getId () { function getId() {
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let id = 'm-' let id = 'm-'
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
@@ -19,20 +20,49 @@ function getId () {
return id return id
} }
function render (element, content, theme) { function render(element, content, theme, enableHTMLLabel) {
try { try {
const height = element.attributes.getNamedItem('data-height') const height = element.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') { const isPredefined = height && height.value !== 'undefined'
if (isPredefined) {
element.style.height = height.value + 'vh' element.style.height = height.value + 'vh'
} }
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
const isDarkTheme = uiThemes.some(
item => item.name === theme && item.isDark
)
mermaidAPI.initialize({ mermaidAPI.initialize({
theme: isDarkTheme ? 'dark' : 'default', theme: isDarkTheme ? 'dark' : 'default',
themeCSS: isDarkTheme ? darkThemeStyling : '', themeCSS: isDarkTheme ? darkThemeStyling : '',
useMaxWidth: false flowchart: {
htmlLabels: enableHTMLLabel
},
gantt: {
useWidth: element.clientWidth
}
}) })
mermaidAPI.render(getId(), content, (svgGraph) => {
mermaidAPI.render(getId(), content, svgGraph => {
element.innerHTML = 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)
console.log(el)
}
}) })
} catch (e) { } catch (e) {
element.className = 'mermaid-error' element.className = 'mermaid-error'

View File

@@ -0,0 +1,78 @@
export const languageMaps = {
brainfuck: 'Brainfuck',
cpp: 'C++',
cs: 'C#',
clojure: 'Clojure',
'clojure-repl': 'ClojureScript',
cmake: 'CMake',
coffeescript: 'CoffeeScript',
crystal: 'Crystal',
css: 'CSS',
d: 'D',
dart: 'Dart',
delphi: 'Pascal',
diff: 'Diff',
django: 'Django',
dockerfile: 'Dockerfile',
ebnf: 'EBNF',
elm: 'Elm',
erlang: 'Erlang',
'erlang-repl': 'Erlang',
fortran: 'Fortran',
fsharp: 'F#',
gherkin: 'Gherkin',
go: 'Go',
groovy: 'Groovy',
haml: 'HAML',
haskell: 'Haskell',
haxe: 'Haxe',
http: 'HTTP',
ini: 'toml',
java: 'Java',
javascript: 'JavaScript',
json: 'JSON',
julia: 'Julia',
kotlin: 'Kotlin',
less: 'LESS',
livescript: 'LiveScript',
lua: 'Lua',
markdown: 'Markdown',
mathematica: 'Mathematica',
nginx: 'Nginx',
nsis: 'NSIS',
objectivec: 'Objective-C',
ocaml: 'Ocaml',
perl: 'Perl',
php: 'PHP',
powershell: 'PowerShell',
properties: 'Properties files',
protobuf: 'ProtoBuf',
python: 'Python',
puppet: 'Puppet',
q: 'Q',
r: 'R',
ruby: 'Ruby',
rust: 'Rust',
sas: 'SAS',
scala: 'Scala',
scheme: 'Scheme',
scss: 'SCSS',
shell: 'Shell',
smalltalk: 'Smalltalk',
sml: 'SML',
sql: 'SQL',
stylus: 'Stylus',
swift: 'Swift',
tcl: 'Tcl',
tex: 'LaTex',
typescript: 'TypeScript',
twig: 'Twig',
vbnet: 'VB.NET',
vbscript: 'VBScript',
verilog: 'Verilog',
vhdl: 'VHDL',
xml: 'HTML',
xquery: 'XQuery',
yaml: 'YAML',
elixir: 'Elixir'
}

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

View File

@@ -11,6 +11,10 @@ const languages = [
name: 'Chinese (zh-TW)', name: 'Chinese (zh-TW)',
locale: 'zh-TW' locale: 'zh-TW'
}, },
{
name: 'Czech',
locale: 'cs'
},
{ {
name: 'Danish', name: 'Danish',
locale: 'da' locale: 'da'
@@ -62,24 +66,25 @@ const languages = [
{ {
name: 'Spanish', name: 'Spanish',
locale: 'es-ES' locale: 'es-ES'
}, { },
{
name: 'Turkish', name: 'Turkish',
locale: 'tr' locale: 'tr'
}, { },
{
name: 'Thai', name: 'Thai',
locale: 'th' locale: 'th'
} }
] ]
module.exports = { module.exports = {
getLocales () { getLocales() {
return languages.reduce(function (localeList, locale) { return languages.reduce(function(localeList, locale) {
localeList.push(locale.locale) localeList.push(locale.locale)
return localeList return localeList
}, []) }, [])
}, },
getLanguages () { getLanguages() {
return languages 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

@@ -0,0 +1,92 @@
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,44 +1,44 @@
import { Point } from '@susisu/mte-kernel' import { Point } from '@susisu/mte-kernel'
export default class TextEditorInterface { export default class TextEditorInterface {
constructor (editor) { constructor(editor) {
this.editor = editor this.editor = editor
this.doc = editor.getDoc() this.doc = editor.getDoc()
this.transaction = false this.transaction = false
} }
getCursorPosition () { getCursorPosition() {
const { line, ch } = this.doc.getCursor() const { line, ch } = this.doc.getCursor()
return new Point(line, ch) return new Point(line, ch)
} }
setCursorPosition (pos) { setCursorPosition(pos) {
this.doc.setCursor({ this.doc.setCursor({
line: pos.row, line: pos.row,
ch: pos.column ch: pos.column
}) })
} }
setSelectionRange (range) { setSelectionRange(range) {
this.doc.setSelection( this.doc.setSelection(
{ line: range.start.row, ch: range.start.column }, { line: range.start.row, ch: range.start.column },
{ line: range.end.row, ch: range.end.column } { line: range.end.row, ch: range.end.column }
) )
} }
getLastRow () { getLastRow() {
return this.doc.lineCount() - 1 return this.doc.lineCount() - 1
} }
acceptsTableEdit () { acceptsTableEdit() {
return true return true
} }
getLine (row) { getLine(row) {
return this.doc.getLine(row) return this.doc.getLine(row)
} }
insertLine (row, line) { insertLine(row, line) {
const lastRow = this.getLastRow() const lastRow = this.getLastRow()
if (row > lastRow) { if (row > lastRow) {
const lastLine = this.getLine(lastRow) const lastLine = this.getLine(lastRow)
@@ -56,7 +56,7 @@ export default class TextEditorInterface {
} }
} }
deleteLine (row) { deleteLine(row) {
const lastRow = this.getLastRow() const lastRow = this.getLastRow()
if (row >= lastRow) { if (row >= lastRow) {
if (lastRow > 0) { if (lastRow > 0) {
@@ -76,15 +76,11 @@ export default class TextEditorInterface {
) )
} }
} else { } else {
this.doc.replaceRange( this.doc.replaceRange('', { line: row, ch: 0 }, { line: row + 1, ch: 0 })
'',
{ line: row, ch: 0 },
{ line: row + 1, ch: 0 }
)
} }
} }
replaceLines (startRow, endRow, lines) { replaceLines(startRow, endRow, lines) {
const lastRow = this.getLastRow() const lastRow = this.getLastRow()
if (endRow > lastRow) { if (endRow > lastRow) {
const lastLine = this.getLine(lastRow) const lastLine = this.getLine(lastRow)
@@ -102,7 +98,7 @@ export default class TextEditorInterface {
} }
} }
transact (func) { transact(func) {
this.transaction = true this.transaction = true
func() func()
this.transaction = false this.transaction = false

View File

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

View File

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

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,6 +1,12 @@
const {remote} = require('electron') import i18n from 'browser/lib/i18n'
const {Menu} = remote.require('electron') 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 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. * Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
@@ -10,11 +16,16 @@ const spellcheck = require('./spellcheck')
* @param {MouseEvent} event that has triggered the creation of the context menu * @param {MouseEvent} event that has triggered the creation of the context menu
* @returns {Electron.Menu} The created electron context menu * @returns {Electron.Menu} The created electron context menu
*/ */
const buildEditorContextMenu = function (editor, event) { const buildEditorContextMenu = function(editor, event) {
if (editor == null || event == null || event.pageX == null || event.pageY == null) { if (
editor == null ||
event == null ||
event.pageX == null ||
event.pageY == null
) {
return null return null
} }
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY}) const cursor = editor.coordsChar({ left: event.pageX, top: event.pageY })
const wordRange = editor.findWordAt(cursor) const wordRange = editor.findWordAt(cursor)
const word = editor.getRange(wordRange.anchor, wordRange.head) const word = editor.getRange(wordRange.anchor, wordRange.head)
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || [] const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
@@ -34,32 +45,108 @@ const buildEditorContextMenu = function (editor, event) {
isMisspelled: isMisspelled, isMisspelled: isMisspelled,
spellingSuggestions: suggestion spellingSuggestions: suggestion
} }
const template = [{ const template = [
role: 'cut' {
}, { role: 'cut'
role: 'copy' },
}, { {
role: 'paste' role: 'copy'
}, { },
role: 'selectall' {
}] role: 'paste'
},
{
role: 'selectall'
}
]
if (selection.isMisspelled) { if (selection.isMisspelled) {
const suggestions = selection.spellingSuggestions const suggestions = selection.spellingSuggestions
template.unshift.apply(template, suggestions.map(function (suggestion) { template.unshift.apply(
return { template,
label: suggestion, suggestions
click: function (suggestion) { .map(function(suggestion) {
if (editor != null) { return {
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head) label: suggestion,
click: function(suggestion) {
if (editor != null) {
editor.replaceRange(
suggestion.label,
wordRange.anchor,
wordRange.head
)
}
}
} }
} })
} .concat({
}).concat({ type: 'separator'
type: 'separator' })
})) )
} }
return Menu.buildFromTemplate(template) return Menu.buildFromTemplate(template)
} }
module.exports = buildEditorContextMenu /**
* 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,4 +1,4 @@
export default function convertModeName (name) { export default function convertModeName(name) {
switch (name) { switch (name) {
case 'ejs': case 'ejs':
return 'Embedded Javascript' return 'Embedded Javascript'

View File

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

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,4 +1,8 @@
export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') { export function findNoteTitle(
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
@@ -6,8 +10,13 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
if (splitted[0] === '---') { if (splitted[0] === '---') {
let line = 0 let line = 0
while (++line < splitted.length) { while (++line < splitted.length) {
if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) { if (
title = splitted[line].substring(frontMatterTitleField.length + 1).trim() enableFrontMatterTitle &&
splitted[line].startsWith(frontMatterTitleField + ':')
) {
title = splitted[line]
.substring(frontMatterTitleField.length + 1)
.trim()
break break
} }
@@ -22,11 +31,15 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
if (title === null) { if (title === null) {
splitted.some((line, index) => { splitted.some((line, index) => {
const trimmedLine = line.trim() const trimmedLine = line.trim()
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim() const trimmedNextLine =
splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
if (trimmedLine.match('```')) { if (trimmedLine.match('```')) {
isInsideCodeBlock = !isInsideCodeBlock isInsideCodeBlock = !isInsideCodeBlock
} }
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) { if (
isInsideCodeBlock === false &&
(trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))
) {
title = trimmedLine title = trimmedLine
return true return true
} }
@@ -35,7 +48,7 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
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,10 +1,11 @@
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)) throw new Error('Target storage doesn\'t exist.') if (!_.isArray(cachedStorageList))
const storage = _.find(cachedStorageList, {key: storageKey}) throw new Error("Target storage doesn't exist.")
if (storage === undefined) throw new Error('Target storage doesn\'t exist.') const storage = _.find(cachedStorageList, { key: storageKey })
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() const trimmedLine = line.trim().replace(/^(>\s*)*/, '')
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) { if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) {
numberOfTodo++ numberOfTodo++
} }
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) { if (trimmedLine.match(/^[+\-*] \[x] ./i)) {
numberOfCompletedTodo++ numberOfCompletedTodo++
} }
}) })
@@ -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

@@ -8,9 +8,10 @@ const i18n = new (require('i18n-2'))({
// setup some locales - other locales default to the first locale // setup some locales - other locales default to the first locale
locales: getLocales(), locales: getLocales(),
extension: '.json', extension: '.json',
directory: process.env.NODE_ENV === 'production' directory:
? path.join(app.getAppPath(), './locales') process.env.NODE_ENV === 'production'
: path.resolve('./locales'), ? path.join(app.getAppPath(), './locales')
: path.resolve('./locales'),
devMode: false devMode: false
}) })

View File

@@ -1,8 +1,7 @@
const crypto = require('crypto') const crypto = require('crypto')
const _ = require('lodash')
const uuidv4 = require('uuid/v4') const uuidv4 = require('uuid/v4')
module.exports = function (uuid) { module.exports = function(uuid) {
if (typeof uuid === typeof true && uuid) { if (typeof uuid === typeof true && uuid) {
return uuidv4() return uuidv4()
} }

View File

@@ -1,35 +1,44 @@
'use strict' 'use strict'
module.exports = function definitionListPlugin (md) { module.exports = function definitionListPlugin(md) {
var isSpace = md.utils.isSpace var isSpace = md.utils.isSpace
// Search `[:~][\n ]`, returns next pos after marker on success // Search `[:~][\n ]`, returns next pos after marker on success
// or -1 on fail. // or -1 on fail.
function skipMarker (state, line) { function skipMarker(state, line) {
let start = state.bMarks[line] + state.tShift[line] let start = state.bMarks[line] + state.tShift[line]
const max = state.eMarks[line] const max = state.eMarks[line]
if (start >= max) { return -1 } if (start >= max) {
return -1
}
// Check bullet // Check bullet
const marker = state.src.charCodeAt(start++) const marker = state.src.charCodeAt(start++)
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 } if (marker !== 0x7e /* ~ */ && marker !== 0x3a /* : */) {
return -1
}
const pos = state.skipSpaces(start) const pos = state.skipSpaces(start)
// require space after ":" // require space after ":"
if (start === pos) { return -1 } if (start === pos) {
return -1
}
return start return start
} }
function markTightParagraphs (state, idx) { function markTightParagraphs(state, idx) {
const level = state.level + 2 const level = state.level + 2
let i let i
let l let l
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) { for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') { if (
state.tokens[i].level === level &&
state.tokens[i].type === 'paragraph_open'
) {
state.tokens[i + 2].hidden = true state.tokens[i + 2].hidden = true
state.tokens[i].hidden = true state.tokens[i].hidden = true
i += 2 i += 2
@@ -37,7 +46,7 @@ module.exports = function definitionListPlugin (md) {
} }
} }
function deflist (state, startLine, endLine, silent) { function deflist(state, startLine, endLine, silent) {
var ch, var ch,
contentStart, contentStart,
ddLine, ddLine,
@@ -63,28 +72,38 @@ module.exports = function definitionListPlugin (md) {
if (silent) { if (silent) {
// quirk: validation mode validates a dd block only, not a whole deflist // quirk: validation mode validates a dd block only, not a whole deflist
if (state.ddIndent < 0) { return false } if (state.ddIndent < 0) {
return false
}
return skipMarker(state, startLine) >= 0 return skipMarker(state, startLine) >= 0
} }
nextLine = startLine + 1 nextLine = startLine + 1
if (nextLine >= endLine) { return false } if (nextLine >= endLine) {
return false
}
if (state.isEmpty(nextLine)) { if (state.isEmpty(nextLine)) {
nextLine++ nextLine++
if (nextLine >= endLine) { return false } if (nextLine >= endLine) {
return false
}
} }
if (state.sCount[nextLine] < state.blkIndent) { return false } if (state.sCount[nextLine] < state.blkIndent) {
return false
}
contentStart = skipMarker(state, nextLine) contentStart = skipMarker(state, nextLine)
if (contentStart < 0) { return false } if (contentStart < 0) {
return false
}
// Start list // Start list
listTokIdx = state.tokens.length listTokIdx = state.tokens.length
tight = true tight = true
token = state.push('dl_open', 'dl', 1) token = state.push('dl_open', 'dl', 1)
token.map = listLines = [ startLine, 0 ] token.map = listLines = [startLine, 0]
// //
// Iterate list items // Iterate list items
@@ -100,34 +119,38 @@ module.exports = function definitionListPlugin (md) {
// needed to break out of the second one // needed to break out of the second one
// //
/* eslint no-labels:0,block-scoped-var:0 */ /* eslint no-labels:0,block-scoped-var:0 */
OUTER: OUTER: for (;;) {
for (;;) {
prevEmptyEnd = false prevEmptyEnd = false
token = state.push('dt_open', 'dt', 1) token = state.push('dt_open', 'dt', 1)
token.map = [ dtLine, dtLine ] token.map = [dtLine, dtLine]
token = state.push('inline', '', 0) token = state.push('inline', '', 0)
token.map = [ dtLine, dtLine ] token.map = [dtLine, dtLine]
token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim() token.content = state
.getLines(dtLine, dtLine + 1, state.blkIndent, false)
.trim()
token.children = [] token.children = []
token = state.push('dt_close', 'dt', -1) token = state.push('dt_close', 'dt', -1)
for (;;) { for (;;) {
token = state.push('dd_open', 'dd', 1) token = state.push('dd_open', 'dd', 1)
token.map = itemLines = [ ddLine, 0 ] token.map = itemLines = [ddLine, 0]
pos = contentStart pos = contentStart
max = state.eMarks[ddLine] max = state.eMarks[ddLine]
offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]) offset =
state.sCount[ddLine] +
contentStart -
(state.bMarks[ddLine] + state.tShift[ddLine])
while (pos < max) { while (pos < max) {
ch = state.src.charCodeAt(pos) ch = state.src.charCodeAt(pos)
if (isSpace(ch)) { if (isSpace(ch)) {
if (ch === 0x09) { if (ch === 0x09) {
offset += 4 - offset % 4 offset += 4 - (offset % 4)
} else { } else {
offset++ offset++
} }
@@ -153,8 +176,11 @@ module.exports = function definitionListPlugin (md) {
state.parentType = 'deflist' state.parentType = 'deflist'
newEndLine = ddLine newEndLine = ddLine
while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) { while (
} ++newEndLine < endLine &&
(state.sCount[newEndLine] >= state.sCount[ddLine] ||
state.isEmpty(newEndLine))
) {}
oldLineMax = state.lineMax oldLineMax = state.lineMax
state.lineMax = newEndLine state.lineMax = newEndLine
@@ -169,7 +195,7 @@ module.exports = function definitionListPlugin (md) {
} }
// Item become loose if finish with empty line, // Item become loose if finish with empty line,
// but we should filter last element, because it means list finish // but we should filter last element, because it means list finish
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1) prevEmptyEnd = state.line - ddLine > 1 && state.isEmpty(state.line - 1)
state.tShift[ddLine] = oldTShift state.tShift[ddLine] = oldTShift
state.sCount[ddLine] = oldSCount state.sCount[ddLine] = oldSCount
@@ -182,11 +208,17 @@ module.exports = function definitionListPlugin (md) {
itemLines[1] = nextLine = state.line itemLines[1] = nextLine = state.line
if (nextLine >= endLine) { break OUTER } if (nextLine >= endLine) {
break OUTER
}
if (state.sCount[nextLine] < state.blkIndent) { break OUTER } if (state.sCount[nextLine] < state.blkIndent) {
break OUTER
}
contentStart = skipMarker(state, nextLine) contentStart = skipMarker(state, nextLine)
if (contentStart < 0) { break } if (contentStart < 0) {
break
}
ddLine = nextLine ddLine = nextLine
@@ -194,20 +226,36 @@ module.exports = function definitionListPlugin (md) {
// insert DD tag and repeat checking // insert DD tag and repeat checking
} }
if (nextLine >= endLine) { break } if (nextLine >= endLine) {
break
}
dtLine = nextLine dtLine = nextLine
if (state.isEmpty(dtLine)) { break } if (state.isEmpty(dtLine)) {
if (state.sCount[dtLine] < state.blkIndent) { break } break
}
if (state.sCount[dtLine] < state.blkIndent) {
break
}
ddLine = dtLine + 1 ddLine = dtLine + 1
if (ddLine >= endLine) { break } if (ddLine >= endLine) {
if (state.isEmpty(ddLine)) { ddLine++ } break
if (ddLine >= endLine) { break } }
if (state.isEmpty(ddLine)) {
ddLine++
}
if (ddLine >= endLine) {
break
}
if (state.sCount[ddLine] < state.blkIndent) { break } if (state.sCount[ddLine] < state.blkIndent) {
break
}
contentStart = skipMarker(state, ddLine) contentStart = skipMarker(state, ddLine)
if (contentStart < 0) { break } if (contentStart < 0) {
break
}
// go to the next loop iteration: // go to the next loop iteration:
// insert DT and DD tags and repeat checking // insert DT and DD tags and repeat checking
@@ -228,5 +276,7 @@ module.exports = function definitionListPlugin (md) {
return true return true
} }
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] }) md.block.ruler.before('paragraph', 'deflist', deflist, {
alt: ['paragraph', 'reference']
})
} }

View File

@@ -1,9 +1,9 @@
'use strict' 'use strict'
module.exports = function (md, renderers, defaultRenderer) { module.exports = function(md, renderers, defaultRenderer) {
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/ const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
function fence (state, startLine, endLine, silent) { function fence(state, startLine, endLine, silent) {
let pos = state.bMarks[startLine] + state.tShift[startLine] let pos = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine] let max = state.eMarks[startLine]
@@ -12,7 +12,7 @@ module.exports = function (md, renderers, defaultRenderer) {
} }
const marker = state.src.charCodeAt(pos) const marker = state.src.charCodeAt(pos)
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) { if (marker !== 0x7e /* ~ */ && marker !== 0x60 /* ` */) {
return false return false
} }
@@ -46,7 +46,10 @@ module.exports = function (md, renderers, defaultRenderer) {
if (pos < max && state.sCount[nextLine] < state.blkIndent) { if (pos < max && state.sCount[nextLine] < state.blkIndent) {
break break
} }
if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) { if (
state.src.charCodeAt(pos) !== marker ||
state.sCount[nextLine] - state.blkIndent >= 4
) {
continue continue
} }
@@ -127,10 +130,12 @@ module.exports = function (md, renderers, defaultRenderer) {
}) })
for (const name in renderers) { for (const name in renderers) {
md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index]) md.renderer.rules[`${name}_fence`] = (tokens, index) =>
renderers[name](tokens[index])
} }
if (defaultRenderer) { if (defaultRenderer) {
md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index]) md.renderer.rules['_fence'] = (tokens, index) =>
defaultRenderer(tokens[index])
} }
} }

View File

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

View File

@@ -4,7 +4,7 @@ import sanitizeHtml from 'sanitize-html'
import { escapeHtmlCharacters } from './utils' import { escapeHtmlCharacters } from './utils'
import url from 'url' import url from 'url'
module.exports = function sanitizePlugin (md, options) { module.exports = function sanitizePlugin(md, options) {
options = options || {} options = options || {}
md.core.ruler.after('linkify', 'sanitize_inline', state => { md.core.ruler.after('linkify', 'sanitize_inline', state => {
@@ -15,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) {
options options
) )
} }
if (state.tokens[tokenIdx].type === '_fence') { if (state.tokens[tokenIdx].type.match(/.*_fence$/)) {
// escapeHtmlCharacters has better performance // escapeHtmlCharacters has better performance
state.tokens[tokenIdx].content = escapeHtmlCharacters( state.tokens[tokenIdx].content = escapeHtmlCharacters(
state.tokens[tokenIdx].content, state.tokens[tokenIdx].content,
@@ -38,15 +38,20 @@ module.exports = function sanitizePlugin (md, 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 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)?/ig const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/gi
function sanitizeInline (html, options) { function sanitizeInline(html, options) {
let match = tagRegex.exec(html) let match = tagRegex.exec(html)
if (!match) { if (!match) {
return '' return ''
} }
const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options const {
allowedTags,
allowedAttributes,
selfClosing,
allowedSchemesAppliedToAttributes
} = options
if (match[1] !== undefined) { if (match[1] !== undefined) {
// opening tag // opening tag
@@ -65,9 +70,17 @@ function sanitizeInline (html, options) {
name = match[1].toLowerCase() name = match[1].toLowerCase()
value = match[3] value = match[3]
if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) { if (
allowedAttributes['*'].indexOf(name) !== -1 ||
(allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)
) {
if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) { if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) { if (
naughtyHRef(value, options) ||
(tag === 'iframe' &&
name === 'src' &&
naughtyIFrame(value, options))
) {
continue continue
} }
} }
@@ -94,8 +107,12 @@ function sanitizeInline (html, options) {
} }
} }
function naughtyHRef (href, options) { function naughtyHRef(href, options) {
// href = href.replace(/[\x00-\x20]+/g, '') // href = href.replace(/[\x00-\x20]+/g, '')
if (!href) {
// No href
return false
}
href = href.replace(/<\!\-\-.*?\-\-\>/g, '') href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
const matches = href.match(/^([a-zA-Z]+)\:/) const matches = href.match(/^([a-zA-Z]+)\:/)
@@ -113,7 +130,7 @@ function naughtyHRef (href, options) {
return options.allowedSchemes.indexOf(scheme) === -1 return options.allowedSchemes.indexOf(scheme) === -1
} }
function naughtyIFrame (src, options) { function naughtyIFrame(src, options) {
try { try {
const parsed = url.parse(src, false, true) const parsed = url.parse(src, false, true)

View File

@@ -12,7 +12,7 @@ const hasProp = Object.prototype.hasOwnProperty
/** /**
* From @enyaxu/markdown-it-anchor * From @enyaxu/markdown-it-anchor
*/ */
function uniqueSlug (slug, slugs, opts) { function uniqueSlug(slug, slugs, opts) {
let uniq = slug let uniq = slug
let i = opts.uniqueSlugStartIndex let i = opts.uniqueSlugStartIndex
while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}` while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
@@ -20,28 +20,24 @@ function uniqueSlug (slug, slugs, opts) {
return uniq return uniq
} }
function linkify (token) { function linkify(token) {
token.content = mdlink(token.content, '#' + token.slug) token.content = mdlink(token.content, `#${decodeURI(token.slug)}`)
return token return token
} }
const TOC_MARKER_START = '<!-- toc -->' const TOC_MARKER_START = '<!-- toc -->'
const TOC_MARKER_END = '<!-- tocstop -->' const TOC_MARKER_END = '<!-- tocstop -->'
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
/** /**
* Takes care of proper updating given editor with TOC. * Takes care of proper updating given editor with TOC.
* If TOC doesn't exit in the editor, it's inserted at current caret position. * If TOC doesn't exit in the editor, it's inserted at current caret position.
* Otherwise,TOC is updated in place. * Otherwise,TOC is updated in place.
* @param editor CodeMirror editor to be updated with TOC * @param editor CodeMirror editor to be updated with TOC
*/ */
export function generateInEditor (editor) { export function generateInEditor(editor) {
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`) function updateExistingToc() {
function tocExistsInEditor () {
return tocRegex.test(editor.getValue())
}
function updateExistingToc () {
const toc = generate(editor.getValue()) const toc = generate(editor.getValue())
const search = editor.getSearchCursor(tocRegex) const search = editor.getSearchCursor(tocRegex)
while (search.findNext()) { while (search.findNext()) {
@@ -49,24 +45,30 @@ export function generateInEditor (editor) {
} }
} }
function addTocAtCursorPosition () { function addTocAtCursorPosition() {
const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity})) const toc = generate(
editor.getRange(editor.getCursor(), { line: Infinity })
)
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor()) editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
} }
if (tocExistsInEditor()) { if (tocExistsInEditor(editor)) {
updateExistingToc() updateExistingToc()
} else { } else {
addTocAtCursorPosition() addTocAtCursorPosition()
} }
} }
export function tocExistsInEditor(editor) {
return tocRegex.test(editor.getValue())
}
/** /**
* Generates MD TOC based on MD document passed as string. * Generates MD TOC based on MD document passed as string.
* @param markdownText MD document * @param markdownText MD document
* @returns generatedTOC String containing generated TOC * @returns generatedTOC String containing generated TOC
*/ */
export function generate (markdownText) { export function generate(markdownText) {
const slugs = {} const slugs = {}
const opts = { const opts = {
uniqueSlugStartIndex: 1 uniqueSlugStartIndex: 1
@@ -86,13 +88,17 @@ export function generate (markdownText) {
return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
} }
function wrapTocWithEol (toc, editor) { function wrapTocWithEol(toc, editor) {
const leftWrap = editor.getCursor().ch === 0 ? '' : EOL const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL const rightWrap =
editor.getLine(editor.getCursor().line).length === editor.getCursor().ch
? ''
: EOL
return leftWrap + toc + rightWrap return leftWrap + toc + rightWrap
} }
export default { export default {
generate, generate,
generateInEditor generateInEditor,
tocExistsInEditor
} }

View File

@@ -2,24 +2,28 @@ import markdownit from 'markdown-it'
import sanitize from './markdown-it-sanitize-html' import sanitize from './markdown-it-sanitize-html'
import emoji from 'markdown-it-emoji' import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math' import math from '@rokt33r/markdown-it-math'
import mdurl from 'mdurl'
import smartArrows from 'markdown-it-smartarrows' import smartArrows from 'markdown-it-smartarrows'
import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor'
import _ from 'lodash' import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex' import katex from 'katex'
import { lastFindInArray } from './utils' import { lastFindInArray } from './utils'
function createGutter (str, firstLineNumber) { function createGutter(str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1 if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1 const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
const lines = [] const lines = []
for (let i = firstLineNumber; i <= lastLineNumber; i++) { for (let i = firstLineNumber; i <= lastLineNumber; i++) {
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>') lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
} }
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>' return (
'<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
)
} }
class Markdown { class Markdown {
constructor (options = {}) { constructor(options = {}) {
const config = ConfigManager.get() const config = ConfigManager.get()
const defaultOptions = { const defaultOptions = {
typographer: config.preview.smartQuotes, typographer: config.preview.smartQuotes,
@@ -32,31 +36,132 @@ class Markdown {
const updatedOptions = Object.assign(defaultOptions, options) const updatedOptions = Object.assign(defaultOptions, options)
this.md = markdownit(updatedOptions) this.md = markdownit(updatedOptions)
this.md.linkify.set({ fuzzyLink: false })
if (updatedOptions.sanitize !== 'NONE') { if (updatedOptions.sanitize !== 'NONE') {
const allowedTags = ['iframe', 'input', 'b', const allowedTags = [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt', 'iframe',
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote', 'input',
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details' '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 = [ const allowedAttributes = [
'abbr', 'accept', 'accept-charset', 'abbr',
'accesskey', 'action', 'align', 'alt', 'axis', 'accept',
'border', 'cellpadding', 'cellspacing', 'char', 'accept-charset',
'charoff', 'charset', 'checked', 'accesskey',
'clear', 'cols', 'colspan', 'color', 'action',
'compact', 'coords', 'datetime', 'dir', 'align',
'disabled', 'enctype', 'for', 'frame', 'alt',
'headers', 'height', 'hreflang', 'axis',
'hspace', 'ismap', 'label', 'lang', 'border',
'maxlength', 'media', 'method', 'cellpadding',
'multiple', 'name', 'nohref', 'noshade', 'cellspacing',
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev', 'char',
'rows', 'rowspan', 'rules', 'scope', 'charoff',
'selected', 'shape', 'size', 'span', 'charset',
'start', 'summary', 'tabindex', 'target', 'checked',
'title', 'type', 'usemap', 'valign', 'value', 'clear',
'vspace', 'width', 'itemprop' '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') { if (updatedOptions.sanitize === 'ALLOW_STYLES') {
@@ -69,20 +174,20 @@ class Markdown {
allowedTags, allowedTags,
allowedAttributes: { allowedAttributes: {
'*': allowedAttributes, '*': allowedAttributes,
'a': ['href'], a: ['href'],
'div': ['itemscope', 'itemtype'], div: ['itemscope', 'itemtype'],
'blockquote': ['cite'], blockquote: ['cite'],
'del': ['cite'], del: ['cite'],
'ins': ['cite'], ins: ['cite'],
'q': ['cite'], q: ['cite'],
'img': ['src', 'width', 'height'], img: ['src', 'width', 'height'],
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'], iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
'input': ['type', 'id', 'checked'] input: ['type', 'id', 'checked']
}, },
allowedIframeHostnames: ['www.youtube.com'], allowedIframeHostnames: ['www.youtube.com'],
selfClosing: [ 'img', 'br', 'hr', 'input' ], selfClosing: ['img', 'br', 'hr', 'input'],
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ], allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ], allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
allowProtocolRelative: true allowProtocolRelative: true
}) })
} }
@@ -95,7 +200,7 @@ class Markdown {
inlineClose: config.preview.latexInlineClose, inlineClose: config.preview.latexInlineClose,
blockOpen: config.preview.latexBlockOpen, blockOpen: config.preview.latexBlockOpen,
blockClose: config.preview.latexBlockClose, blockClose: config.preview.latexBlockClose,
inlineRenderer: function (str) { inlineRenderer: function(str) {
let output = '' let output = ''
try { try {
output = katex.renderToString(str.trim()) output = katex.renderToString(str.trim())
@@ -104,7 +209,7 @@ class Markdown {
} }
return output return output
}, },
blockRenderer: function (str) { blockRenderer: function(str) {
let output = '' let output = ''
try { try {
output = katex.renderToString(str.trim(), { displayMode: true }) output = katex.renderToString(str.trim(), { displayMode: true })
@@ -121,96 +226,167 @@ class Markdown {
slugify: require('./slugify') slugify: require('./slugify')
}) })
this.md.use(require('markdown-it-kbd')) this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']}) this.md.use(require('markdown-it-admonition'), {
types: [
'note',
'hint',
'attention',
'caution',
'danger',
'error',
'quote',
'abstract',
'question'
]
})
this.md.use(require('markdown-it-abbr')) this.md.use(require('markdown-it-abbr'))
this.md.use(require('markdown-it-sub')) this.md.use(require('markdown-it-sub'))
this.md.use(require('markdown-it-sup')) this.md.use(require('markdown-it-sup'))
this.md.use(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-deflist'))
this.md.use(require('./markdown-it-frontmatter')) this.md.use(require('./markdown-it-frontmatter'))
this.md.use(require('./markdown-it-fence'), { this.md.use(
chart: token => { require('./markdown-it-fence'),
if (token.parameters.hasOwnProperty('yaml')) { {
token.parameters.format = 'yaml' chart: token => {
} if (token.parameters.hasOwnProperty('yaml')) {
token.parameters.format = 'yaml'
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 => {
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 match[1]
} else {
return line
} }
}).join('\n')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="gallery" data-autoplay="${token.parameters.autoplay}" data-height="${token.parameters.height}">${content}</div> <div class="chart" data-height="${
token.parameters.height
}" data-format="${token.parameters.format || 'json'}">${
token.content
}</div>
</pre>` </pre>`
},
flowchart: token => {
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 => {
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 => {
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>`
}
}, },
mermaid: token => { token => {
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="code CodeMirror" 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 => {
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 => {
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
${createGutter(token.content, token.firstLineNumber)} ${createGutter(token.content, token.firstLineNumber)}
<code class="${token.langType}">${token.content}</code> <code class="${token.langType}">${token.content}</code>
</pre>` </pre>`
}) }
)
const deflate = require('markdown-it-plantuml/lib/deflate') const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', { const plantuml = require('markdown-it-plantuml')
generateSource: function (umlCode) { const plantUmlStripTrailingSlash = url =>
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url url.endsWith('/') ? url.slice(0, -1) : url
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg' const plantUmlServerAddress = plantUmlStripTrailingSlash(
const s = unescape(encodeURIComponent(umlCode)) config.preview.plantUMLServerAddress
const zippedCode = deflate.encode64( )
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9) const parsePlantUml = function(umlCode, openMarker, closeMarker, type) {
) const s = unescape(encodeURIComponent(umlCode))
return `${serverAddress}/${zippedCode}` const zippedCode = deflate.encode64(
} deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9)
)
return `${plantUmlServerAddress}/${type}/${zippedCode}`
}
this.md.use(plantuml, {
generateSource: umlCode =>
parsePlantUml(umlCode, '@startuml', '@enduml', 'svg')
}) })
// Ditaa support // Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
this.md.use(require('markdown-it-plantuml'), { this.md.use(plantuml, {
openMarker: '@startditaa', openMarker: '@startditaa',
closeMarker: '@endditaa', closeMarker: '@endditaa',
generateSource: function (umlCode) { generateSource: umlCode =>
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment. })
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
const s = unescape(encodeURIComponent(umlCode)) // Mindmap support
const zippedCode = deflate.encode64( this.md.use(plantuml, {
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9) openMarker: '@startmindmap',
) closeMarker: '@endmindmap',
return `${serverAddress}/${zippedCode}` generateSource: umlCode =>
} parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg')
})
// WBS support
this.md.use(plantuml, {
openMarker: '@startwbs',
closeMarker: '@endwbs',
generateSource: umlCode =>
parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg')
})
// Gantt support
this.md.use(plantuml, {
openMarker: '@startgantt',
closeMarker: '@endgantt',
generateSource: umlCode =>
parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg')
}) })
// Override task item // Override task item
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) { this.md.block.ruler.at('paragraph', function(
state,
startLine /*, endLine */
) {
let content, terminate, i, l, token let content, terminate, i, l, token
let nextLine = startLine + 1 let nextLine = startLine + 1
const terminatorRules = state.md.block.ruler.getRules('paragraph') const terminatorRules = state.md.block.ruler.getRules('paragraph')
@@ -220,10 +396,14 @@ class Markdown {
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
// this would be a code block normally, but after paragraph // this would be a code block normally, but after paragraph
// it's considered a lazy continuation regardless of what's there // it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) { continue } if (state.sCount[nextLine] - state.blkIndent > 3) {
continue
}
// quirk for blockquotes, this line should already be checked by that rule // quirk for blockquotes, this line should already be checked by that rule
if (state.sCount[nextLine] < 0) { continue } if (state.sCount[nextLine] < 0) {
continue
}
// Some tags can terminate paragraph without empty line. // Some tags can terminate paragraph without empty line.
terminate = false terminate = false
@@ -233,10 +413,14 @@ class Markdown {
break break
} }
} }
if (terminate) { break } if (terminate) {
break
}
} }
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() content = state
.getLines(startLine, nextLine, state.blkIndent, false)
.trim()
state.line = nextLine state.line = nextLine
@@ -246,18 +430,31 @@ class Markdown {
if (state.parentType === 'list') { if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i) const match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) { if (match) {
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open') const liToken = lastFindInArray(
state.tokens,
token => token.type === 'list_item_open'
)
if (liToken) { if (liToken) {
if (!liToken.attrs) { if (!liToken.attrs) {
liToken.attrs = [] liToken.attrs = []
} }
if (config.preview.lineThroughCheckbox) { if (config.preview.lineThroughCheckbox) {
liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`]) liToken.attrs.push([
'class',
`taskListItem${match[1] !== ' ' ? ' checked' : ''}`
])
} else { } else {
liToken.attrs.push(['class', 'taskListItem']) liToken.attrs.push(['class', 'taskListItem'])
} }
} }
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>` content = `<label class='taskListItem${
match[1] !== ' ' ? ' checked' : ''
}' for='checkbox-${startLine + 1}'><input type='checkbox'${
match[1] !== ' ' ? ' checked' : ''
} id='checkbox-${startLine + 1}'/> ${content.substring(
4,
content.length
)}</label>`
} }
} }
@@ -278,7 +475,7 @@ class Markdown {
// Add line number attribute for scrolling // Add line number attribute for scrolling
const originalRender = this.md.renderer.render const originalRender = this.md.renderer.render
this.md.renderer.render = (tokens, options, env) => { this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => { tokens.forEach(token => {
switch (token.type) { switch (token.type) {
case 'blockquote_open': case 'blockquote_open':
case 'dd_open': case 'dd_open':
@@ -287,7 +484,9 @@ class Markdown {
case 'list_item_open': case 'list_item_open':
case 'paragraph_open': case 'paragraph_open':
case 'table_open': case 'table_open':
token.attrPush(['data-line', token.map[0]]) if (token.map) {
token.attrPush(['data-line', token.map[0]])
}
} }
}) })
const result = originalRender.call(this.md.renderer, tokens, options, env) const result = originalRender.call(this.md.renderer, tokens, options, env)
@@ -297,7 +496,7 @@ class Markdown {
window.md = this.md window.md = this.md
} }
render (content) { render(content) {
if (!_.isString(content)) content = '' if (!_.isString(content)) content = ''
return this.md.render(content) return this.md.render(content)
} }

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*([^#]*)\s*(#{1,6})?/gm, '$1') .replace(/^#{1,6}\s*/gm, '')
.replace(/(`{3,})(.*?)\1/gm, '$2') .replace(/(`{3,})(.*?)\1/gm, '$2')
.replace(/^-{3,}\s*$/g, '') .replace(/^-{3,}\s*$/g, '')
.replace(/`(.+?)`/g, '$1') .replace(/`(.+?)`/g, '$1')

View File

@@ -1,14 +1,25 @@
import { hashHistory } from 'react-router'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import queryString from 'query-string'
import { push } from 'connected-react-router'
export function createMarkdownNote (storage, folder, dispatch, location, params, config) { export function createMarkdownNote(
storage,
folder,
dispatch,
location,
params,
config
) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let tags = [] let tags = []
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) { if (
config.ui.tagNewNoteWithFilteringTags &&
location.pathname.match(/\/tags/)
) {
tags = params.tagname.split(' ') tags = params.tagname.split(' ')
} }
@@ -28,24 +39,41 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
note: note note: note
}) })
hashHistory.push({ dispatch(
pathname: location.pathname, push({
query: { key: noteHash } pathname: location.pathname,
}) search: queryString.stringify({ key: noteHash })
})
)
ee.emit('list:jump', noteHash) ee.emit('list:jump', noteHash)
ee.emit('detail:focus') ee.emit('detail:focus')
}) })
} }
export function createSnippetNote (storage, folder, dispatch, location, params, config) { export function createSnippetNote(
storage,
folder,
dispatch,
location,
params,
config
) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let tags = [] let tags = []
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) { if (
config.ui.tagNewNoteWithFilteringTags &&
location.pathname.match(/\/tags/)
) {
tags = params.tagname.split(' ') tags = params.tagname.split(' ')
} }
const defaultLanguage =
config.editor.snippetDefaultLanguage === 'Auto Detect'
? null
: config.editor.snippetDefaultLanguage
return dataApi return dataApi
.createNote(storage, { .createNote(storage, {
type: 'SNIPPET_NOTE', type: 'SNIPPET_NOTE',
@@ -56,7 +84,7 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
snippets: [ snippets: [
{ {
name: '', name: '',
mode: config.editor.snippetDefaultLanguage || 'text', mode: defaultLanguage,
content: '', content: '',
linesHighlighted: [] linesHighlighted: []
} }
@@ -68,10 +96,12 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
}) })
hashHistory.push({ dispatch(
pathname: location.pathname, push({
query: { key: noteHash } pathname: location.pathname,
}) search: queryString.stringify({ key: noteHash })
})
)
ee.emit('list:jump', noteHash) ee.emit('list:jump', noteHash)
ee.emit('detail:focus') ee.emit('detail:focus')
}) })

View File

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

View File

@@ -1,31 +1,38 @@
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 => { return block !== '' }) const searchBlocks = search.split(' ').filter(block => {
return block !== ''
})
let foundNotes = notes let foundNotes = notes
searchBlocks.forEach((block) => { searchBlocks.forEach(block => {
foundNotes = findByWordOrTag(foundNotes, block) foundNotes = findByWordOrTag(foundNotes, block)
}) })
return foundNotes return foundNotes
} }
function findByWordOrTag (notes, block) { function findByWordOrTag(notes, block) {
let tag = block let tag = block
if (tag.match(/^#.+/)) { if (tag.match(/^#.+/)) {
tag = tag.match(/#(.+)/)[1] tag = tag.match(/#(.+)/)[1]
} }
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i') const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i') const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
return notes.filter((note) => { return notes.filter(note => {
if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) { if (_.isArray(note.tags) && note.tags.some(_tag => _tag.match(tagRegExp))) {
return true return true
} }
if (note.type === 'SNIPPET_NOTE') { if (note.type === 'SNIPPET_NOTE') {
return note.description.match(wordRegExp) || note.snippets.some((snippet) => { return (
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp) 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(wordRegExp)
} }

View File

@@ -1,17 +1,15 @@
import diacritics from 'diacritics-map' module.exports = function slugify(title) {
const slug = encodeURI(
title
.trim()
.replace(/^\s+/, '')
.replace(/\s+$/, '')
.replace(/\s+/g, '-')
.replace(
/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g,
''
)
)
function replaceDiacritics (str) { return slug
return str.replace(/[À-ž]/g, function (ch) {
return diacritics[ch] || ch
})
}
module.exports = function slugify (title) {
let slug = title.trim()
slug = replaceDiacritics(slug)
slug = slug.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
return encodeURI(slug).replace(/\-+$/, '')
} }

View File

@@ -12,19 +12,19 @@ const MILLISECONDS_TILL_LIVECHECK = 500
let dictionary = null let dictionary = null
let self let self
function getAvailableDictionaries () { function getAvailableDictionaries() {
return [ return [
{label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED}, { label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED },
{label: i18n.__('English'), value: 'en_GB'}, { label: i18n.__('English'), value: 'en_GB' },
{label: i18n.__('German'), value: 'de_DE'}, { label: i18n.__('German'), value: 'de_DE' },
{label: i18n.__('French'), value: 'fr_FR'} { label: i18n.__('French'), value: 'fr_FR' }
] ]
} }
/** /**
* Only to be used in the tests :) * Only to be used in the tests :)
*/ */
function setDictionaryForTestsOnly (newDictionary) { function setDictionaryForTestsOnly(newDictionary) {
dictionary = newDictionary dictionary = newDictionary
} }
@@ -34,7 +34,7 @@ function setDictionaryForTestsOnly (newDictionary) {
* @param {Codemirror} editor CodeMirror-Editor * @param {Codemirror} editor CodeMirror-Editor
* @param {String} lang on of the values from getAvailableDictionaries()-Method * @param {String} lang on of the values from getAvailableDictionaries()-Method
*/ */
function setLanguage (editor, lang) { function setLanguage(editor, lang) {
self = this self = this
dictionary = null dictionary = null
@@ -50,8 +50,7 @@ function setLanguage (editor, lang) {
dictionary = new Typo(lang, false, false, { dictionary = new Typo(lang, false, false, {
dictionaryPath: DICTIONARY_PATH, dictionaryPath: DICTIONARY_PATH,
asyncLoad: true, asyncLoad: true,
loadedCallback: () => loadedCallback: () => checkWholeDocument(editor)
checkWholeDocument(editor)
}) })
} }
} }
@@ -60,12 +59,12 @@ function setLanguage (editor, lang) {
* Checks the whole content of the editor for typos * Checks the whole content of the editor for typos
* @param {Codemirror} editor CodeMirror-Editor * @param {Codemirror} editor CodeMirror-Editor
*/ */
function checkWholeDocument (editor) { function checkWholeDocument(editor) {
const lastLine = editor.lineCount() - 1 const lastLine = editor.lineCount() - 1
const textOfLastLine = editor.getLine(lastLine) || '' const textOfLastLine = editor.getLine(lastLine) || ''
const lastChar = textOfLastLine.length const lastChar = textOfLastLine.length
const from = {line: 0, ch: 0} const from = { line: 0, ch: 0 }
const to = {line: lastLine, ch: lastChar} const to = { line: lastLine, ch: lastChar }
checkMultiLineRange(editor, from, to) checkMultiLineRange(editor, from, to)
} }
@@ -75,15 +74,18 @@ function checkWholeDocument (editor) {
* @param {line, ch} from starting position of the spellcheck * @param {line, ch} from starting position of the spellcheck
* @param {line, ch} to end position of the spellcheck * @param {line, ch} to end position of the spellcheck
*/ */
function checkMultiLineRange (editor, from, to) { function checkMultiLineRange(editor, from, to) {
function sortRange (pos1, pos2) { function sortRange(pos1, pos2) {
if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) { if (
return {from: pos2, to: pos1} pos1.line > pos2.line ||
(pos1.line === pos2.line && pos1.ch > pos2.ch)
) {
return { from: pos2, to: pos1 }
} }
return {from: pos1, to: pos2} return { from: pos1, to: pos2 }
} }
const {from: smallerPos, to: higherPos} = sortRange(from, to) const { from: smallerPos, to: higherPos } = sortRange(from, to)
for (let l = smallerPos.line; l <= higherPos.line; l++) { for (let l = smallerPos.line; l <= higherPos.line; l++) {
const line = editor.getLine(l) || '' const line = editor.getLine(l) || ''
let w = 0 let w = 0
@@ -95,9 +97,9 @@ function checkMultiLineRange (editor, from, to) {
wEnd = higherPos.ch wEnd = higherPos.ch
} }
while (w <= wEnd) { while (w <= wEnd) {
const wordRange = editor.findWordAt({line: l, ch: w}) const wordRange = editor.findWordAt({ line: l, ch: w })
self.checkWord(editor, wordRange) self.checkWord(editor, wordRange)
w += (wordRange.head.ch - wordRange.anchor.ch) + 1 w += wordRange.head.ch - wordRange.anchor.ch + 1
} }
} }
} }
@@ -110,13 +112,15 @@ function checkMultiLineRange (editor, from, to) {
* @param wordRange Object specifying the range that should be checked. * @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> * Having the following structure: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
*/ */
function checkWord (editor, wordRange) { function checkWord(editor, wordRange) {
const word = editor.getRange(wordRange.anchor, wordRange.head) const word = editor.getRange(wordRange.anchor, wordRange.head)
if (word == null || word.length <= 3) { if (word == null || word.length <= 3) {
return return
} }
if (!dictionary.check(word)) { if (!dictionary.check(word)) {
editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]}) editor.markText(wordRange.anchor, wordRange.head, {
className: styles[CSS_ERROR_CLASS]
})
} }
} }
@@ -126,32 +130,40 @@ function checkWord (editor, wordRange) {
* @param fromChangeObject codeMirror changeObject describing the start of the editing * @param fromChangeObject codeMirror changeObject describing the start of the editing
* @param toChangeObject codeMirror changeObject describing the end of the editing * @param toChangeObject codeMirror changeObject describing the end of the editing
*/ */
function checkChangeRange (editor, fromChangeObject, toChangeObject) { function checkChangeRange(editor, fromChangeObject, toChangeObject) {
/** /**
* Calculate the smallest respectively largest position as a start, resp. end, position and return it * Calculate the smallest respectively largest position as a start, resp. end, position and return it
* @param start CodeMirror change object * @param start CodeMirror change object
* @param end CodeMirror change object * @param end CodeMirror change object
* @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}} * @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
*/ */
function getStartAndEnd (start, end) { function getStartAndEnd(start, end) {
const possiblePositions = [start.from, start.to, end.from, end.to] const possiblePositions = [start.from, start.to, end.from, end.to]
let smallest = start.from let smallest = start.from
let biggest = end.to let biggest = end.to
for (const currentPos of possiblePositions) { for (const currentPos of possiblePositions) {
if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) { if (
currentPos.line < smallest.line ||
(currentPos.line === smallest.line && currentPos.ch < smallest.ch)
) {
smallest = currentPos smallest = currentPos
} }
if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) { if (
currentPos.line > biggest.line ||
(currentPos.line === biggest.line && currentPos.ch > biggest.ch)
) {
biggest = currentPos biggest = currentPos
} }
} }
return {start: smallest, end: biggest} return { start: smallest, end: biggest }
} }
if (dictionary === null || editor == null) { return } if (dictionary === null || editor == null) {
return
}
try { try {
const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject) const { start, end } = getStartAndEnd(fromChangeObject, toChangeObject)
// Expand the range to include words after/before whitespaces // Expand the range to include words after/before whitespaces
start.ch = Math.max(start.ch - 1, 0) start.ch = Math.max(start.ch - 1, 0)
@@ -165,29 +177,40 @@ function checkChangeRange (editor, fromChangeObject, toChangeObject) {
self.checkMultiLineRange(editor, start, end) self.checkMultiLineRange(editor, start, end)
} catch (e) { } catch (e) {
console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', 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) { function saveLiveSpellCheckFrom(changeObject) {
liveSpellCheckFrom = changeObject liveSpellCheckFrom = changeObject
} }
let liveSpellCheckFrom let liveSpellCheckFrom
const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, { const debouncedSpellCheckLeading = _.debounce(
'leading': true, saveLiveSpellCheckFrom,
'trailing': false MILLISECONDS_TILL_LIVECHECK,
}) {
const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, { leading: true,
'leading': false, trailing: false
'trailing': true }
}) )
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 * 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 {Codemirror} editor CodeMirror-Editor
* @param changeObject codeMirror changeObject * @param changeObject codeMirror changeObject
*/ */
function handleChange (editor, changeObject) { function handleChange(editor, changeObject) {
if (dictionary === null) { if (dictionary === null) {
return return
} }
@@ -201,7 +224,7 @@ function handleChange (editor, changeObject) {
* @param word word to be checked * @param word word to be checked
* @returns {String[]} Array of suggestions * @returns {String[]} Array of suggestions
*/ */
function getSpellingSuggestion (word) { function getSpellingSuggestion(word) {
if (dictionary == null || word == null) { if (dictionary == null || word == null) {
return [] return []
} }
@@ -211,7 +234,7 @@ function getSpellingSuggestion (word) {
/** /**
* Returns the name of the CSS class used for errors * Returns the name of the CSS class used for errors
*/ */
function getCSSClassName () { function getCSSClassName() {
return styles[CSS_ERROR_CLASS] return styles[CSS_ERROR_CLASS]
} }

9
browser/lib/turndown.js Normal file
View File

@@ -0,0 +1,9 @@
const TurndownService = require('turndown')
const { gfm } = require('turndown-plugin-gfm')
export const createTurndownService = function() {
const turndown = new TurndownService()
turndown.use(gfm)
turndown.remove('script')
return turndown
}

44
browser/lib/ui-themes.js Normal file
View File

@@ -0,0 +1,44 @@
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,4 +1,4 @@
export function lastFindInArray (array, callback) { export function lastFindInArray(array, callback) {
for (let i = array.length - 1; i >= 0; --i) { for (let i = array.length - 1; i >= 0; --i) {
if (callback(array[i], i, array)) { if (callback(array[i], i, array)) {
return array[i] return array[i]
@@ -6,7 +6,7 @@ export function lastFindInArray (array, callback) {
} }
} }
export function escapeHtmlCharacters ( export function escapeHtmlCharacters(
html, html,
opt = { detectCodeBlock: false, skipSingleQuote: false } opt = { detectCodeBlock: false, skipSingleQuote: false }
) { ) {
@@ -115,7 +115,7 @@ export function escapeHtmlCharacters (
return html return html
} }
export function isObjectEqual (a, b) { export function isObjectEqual(a, b) {
const aProps = Object.getOwnPropertyNames(a) const aProps = Object.getOwnPropertyNames(a)
const bProps = Object.getOwnPropertyNames(b) const bProps = Object.getOwnPropertyNames(b)
@@ -132,8 +132,30 @@ export function isObjectEqual (a, b) {
return true return true
} }
export function isMarkdownTitleURL(str) {
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(
str
)
}
export function humanFileSize(bytes) {
const threshold = 1000
if (Math.abs(bytes) < threshold) {
return bytes + ' B'
}
var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var u = -1
do {
bytes /= threshold
++u
} while (Math.abs(bytes) >= threshold && u < units.length - 1)
return bytes.toFixed(1) + ' ' + units[u]
}
export default { export default {
lastFindInArray, lastFindInArray,
escapeHtmlCharacters, escapeHtmlCharacters,
isObjectEqual isObjectEqual,
isMarkdownTitleURL,
humanFileSize
} }

View File

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

View File

@@ -6,7 +6,7 @@ import _ from 'lodash'
import i18n from 'browser/lib/i18n' 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,24 +16,27 @@ 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', {
optionIndex: -1 status: 'SEARCH',
}, () => { 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'
@@ -41,7 +44,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'
@@ -49,40 +52,49 @@ 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', {
optionIndex: -1 status: 'SEARCH',
}, () => { 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', {
optionIndex: 0 status: 'SEARCH',
}, () => { 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('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])') const tabbable = document.querySelectorAll(
const previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 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]
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'
@@ -90,14 +102,17 @@ 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 = search.length > 0 const optionIndex =
? _.findIndex(folders, (folder) => { search.length > 0
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i')) ? _.findIndex(folders, folder => {
}) return folder.name.match(
: -1 new RegExp('^' + _.escapeRegExp(search), 'i')
)
})
: -1
this.setState({ this.setState({
search: this.refs.search.value, search: this.refs.search.value,
@@ -105,7 +120,7 @@ class FolderSelect extends React.Component {
}) })
} }
handleSearchInputKeyDown (e) { handleSearchInputKeyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
case 40: case 40:
e.stopPropagation() e.stopPropagation()
@@ -121,15 +136,18 @@ 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
@@ -141,7 +159,7 @@ class FolderSelect extends React.Component {
}) })
} }
previousOption () { previousOption() {
const { folders } = this.props const { folders } = this.props
let { optionIndex } = this.state let { optionIndex } = this.state
@@ -153,46 +171,52 @@ 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
@@ -200,68 +224,78 @@ class FolderSelect extends React.Component {
}) })
}) })
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0] const currentOption = options.filter(
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 const optionList = options.map((option, index) => {
.map((option, index) => { return (
return ( <div
<div styleName={index === this.state.optionIndex styleName={
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) => this.handleOptionClick(option.storage.key, option.folder.key)(e)} onClick={e =>
this.handleOptionClick(option.storage.key, option.folder.key)(e)
}
>
<span
styleName='search-optionList-item-name'
style={{ borderColor: option.folder.color }}
> >
<span styleName='search-optionList-item-name' {option.folder.name}
style={{borderColor: option.folder.color}} <span styleName='search-optionList-item-name-surfix'>
> in {option.storage.name}
{option.folder.name}
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span>
</span> </span>
</div> </span>
) </div>
}) )
})
return ( return (
<div className={_.isString(className) <div
? 'FolderSelect ' + className className={
: 'FolderSelect' _.isString(className) ? 'FolderSelect ' + className : 'FolderSelect'
} }
styleName={this.state.status === 'SEARCH' styleName={
? 'root--search' this.state.status === 'SEARCH'
: this.state.status === 'FOCUS' ? 'root--search'
? 'root--focus' : this.state.status === 'FOCUS'
: 'root' ? 'root--focus'
: '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 styleName='search-input' <input
styleName='search-input'
ref='search' ref='search'
value={this.state.search} value={this.state.search}
placeholder={i18n.__('Folder...')} placeholder={i18n.__('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' <div styleName='search-optionList' ref='optionList'>
ref='optionList'
>
{optionList} {optionList}
</div> </div>
</div> </div>
: <div styleName='idle' style={{color: currentOption.folder.color}}> ) : currentOption ? (
<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'>
@@ -269,8 +303,7 @@ class FolderSelect extends React.Component {
</span> </span>
</div> </div>
</div> </div>
} ) : null}
</div> </div>
) )
} }
@@ -280,11 +313,13 @@ FolderSelect.propTypes = {
className: PropTypes.string, className: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
value: PropTypes.string, value: PropTypes.string,
folders: PropTypes.arrayOf(PropTypes.shape({ folders: PropTypes.arrayOf(
key: PropTypes.string, PropTypes.shape({
name: PropTypes.string, key: PropTypes.string,
color: PropTypes.string name: PropTypes.string,
})) color: PropTypes.string
})
)
} }
export default CSSModules(FolderSelect, styles) export default CSSModules(FolderSelect, styles)

View File

@@ -134,54 +134,39 @@ body[data-theme="dark"]
.search-optionList-item-name-surfix .search-optionList-item-name-surfix
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
body[data-theme="monokai"] apply-theme(theme)
.root body[data-theme={theme}]
color $ui-dark-text-color .root
&:hover &:hover
color white background-color get-theme-var(theme, 'button--hover-backgroundColor')
background-color $ui-monokai-button--hover-backgroundColor border-color get-theme-var(theme, 'borderColor')
border-color $ui-monokai-borderColor
.search-optionList .search-input
color white color get-theme-var(theme, 'text-color')
border-color $ui-monokai-borderColor background-color transparent
background-color $ui-monokai-button-backgroundColor border-color get-theme-var(theme, 'borderColor')
.search-optionList-item .search-optionList
&:hover color get-theme-var(theme, 'text-color')
background-color lighten($ui-monokai-button--hover-backgroundColor, 15%) border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'button-backgroundColor')
.search-optionList-item--active .search-optionList-item
background-color $ui-monokai-button--active-backgroundColor &:hover
color $ui-monokai-button--active-color background-color lighten(get-theme-var(theme, 'button--hover-backgroundColor'), 15%)
&:hover
background-color $ui-monokai-button--active-backgroundColor
color $ui-monokai-button--active-color
.search-optionList-item-name-surfix
color $ui-monokai-inactive-text-color
body[data-theme="dracula"] .search-optionList-item--active
.root background-color get-theme-var(theme, 'button--active-backgroundColor')
color $ui-dracula-text-color color get-theme-var(theme, 'button--active-color')
&:hover &:hover
color #f8f8f2 background-color get-theme-var(theme, 'button--active-backgroundColor')
background-color $ui-dark-button--hover-backgroundColor color get-theme-var(theme, 'button--active-color')
border-color $ui-dracula-borderColor
.search-optionList .search-optionList-item-name-surfix
color #f8f8f2 color get-theme-var(theme, 'inactive-text-color')
border-color $ui-dracula-borderColor
background-color $ui-dracula-button-backgroundColor
.search-optionList-item for theme in 'solarized-dark' 'dracula'
&:hover apply-theme(theme)
background-color lighten($ui-dracula-button--hover-backgroundColor, 15%)
.search-optionList-item--active for theme in $themes
background-color $ui-dracula-button--active-backgroundColor apply-theme(theme)
color $ui-dracula-button--active-color
&:hover
background-color $ui-dark-button--hover-backgroundColor
color $ui-dracula-button--active-color
.search-optionList-item-name-surfix
color $ui-dracula-inactive-text-color

View File

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

@@ -0,0 +1,41 @@
.root
top 45px
topBarButtonRight()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 125px
width 90px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.root--active
@extend .root
transition 0.15s
color $ui-favorite-star-button-color
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
.icon
transition transform 0.15s
height 13px
body[data-theme="dark"]
.root
topBarButtonDark()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)

View File

@@ -5,15 +5,21 @@ import styles from './FullscreenButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B' const FullscreenButton = ({ onClick }) => {
const FullscreenButton = ({ const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
onClick return (
}) => ( <button
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}> styleName='control-fullScreenButton'
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' /> title={i18n.__('Fullscreen')}
<span styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span> onMouseDown={e => onClick(e)}
</button> >
) <img src='../resources/icon/icon-full.svg' />
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Fullscreen')}({hotkey})
</span>
</button>
)
}
FullscreenButton.propTypes = { FullscreenButton.propTypes = {
onClick: PropTypes.func.isRequired onClick: PropTypes.func.isRequired

View File

@@ -1,22 +1,26 @@
.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
body[data-theme="dark"] .tooltip:lang(ja)
.control-fullScreenButton @extend .tooltip
right 35px
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark() topBarButtonDark()

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoButton.styl' import styles from './InfoButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const InfoButton = ({ const InfoButton = ({ onClick }) => (
onClick <button styleName='control-infoButton' onClick={e => onClick(e)}>
}) => (
<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'>{i18n.__('Info')}</span>
</button> </button>

View File

@@ -6,28 +6,47 @@ import copy from 'copy-to-clipboard'
import i18n from 'browser/lib/i18n' 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, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print storageName,
folderName,
noteLink,
updatedAt,
createdAt,
exportAsMd,
exportAsTxt,
exportAsHtml,
exportAsPdf,
wordCount,
letterCount,
type,
print
} = this.props } = this.props
return ( return (
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}> <div
className='infoPanel'
styleName='control-infoButton-panel'
style={{ display: 'none' }}
>
<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'>
{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'>{i18n.__('Words')}</p>
@@ -37,12 +56,9 @@ class InfoPanel extends React.Component {
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p> <p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
</div> </div>
</div> </div>
} )}
{type === 'SNIPPET_NOTE' {type === 'SNIPPET_NOTE' ? '' : <hr />}
? ''
: <hr />
}
<div> <div>
<p styleName='infoPanel-default'>{storageName}</p> <p styleName='infoPanel-default'>{storageName}</p>
@@ -60,8 +76,18 @@ class InfoPanel extends React.Component {
</div> </div>
<div> <div>
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} /> <input
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'> styleName='infoPanel-noteLink'
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'>{i18n.__('NOTE LINK')}</p>
@@ -70,22 +96,39 @@ class InfoPanel extends React.Component {
<hr /> <hr />
<div id='export-wrap'> <div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}> <button
styleName='export--enable'
onClick={e => exportAsMd(e, 'export-md')}
>
<i className='fa fa-file-code-o' /> <i className='fa fa-file-code-o' />
<p>{i18n.__('.md')}</p> <p>{i18n.__('.md')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}> <button
styleName='export--enable'
onClick={e => exportAsTxt(e, 'export-txt')}
>
<i className='fa fa-file-text-o' /> <i className='fa fa-file-text-o' />
<p>{i18n.__('.txt')}</p> <p>{i18n.__('.txt')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}> <button
styleName='export--enable'
onClick={e => exportAsHtml(e, 'export-html')}
>
<i className='fa fa-html5' /> <i className='fa fa-html5' />
<p>{i18n.__('.html')}</p> <p>{i18n.__('.html')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => print(e, 'print')}> <button
styleName='export--enable'
onClick={e => exportAsPdf(e, 'export-pdf')}
>
<i className='fa fa-file-pdf-o' />
<p>{i18n.__('.pdf')}</p>
</button>
<button styleName='export--enable' onClick={e => print(e, 'print')}>
<i className='fa fa-print' /> <i className='fa fa-print' />
<p>{i18n.__('Print')}</p> <p>{i18n.__('Print')}</p>
</button> </button>
@@ -104,6 +147,7 @@ InfoPanel.propTypes = {
exportAsMd: PropTypes.func.isRequired, exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired, exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired, exportAsHtml: PropTypes.func.isRequired,
exportAsPdf: PropTypes.func.isRequired,
wordCount: PropTypes.number, wordCount: PropTypes.number,
letterCount: PropTypes.number, letterCount: PropTypes.number,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,

View File

@@ -15,7 +15,7 @@
right 25px right 25px
position absolute position absolute
padding 20px 25px 0 25px padding 20px 25px 0 25px
width 300px // width 300px
overflow auto overflow auto
background-color $ui-noteList-backgroundColor background-color $ui-noteList-backgroundColor
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1) box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
@@ -138,162 +138,49 @@
.export--unable .export--unable
cursor not-allowed cursor not-allowed
body[data-theme="dark"] apply-theme(theme)
.control-infoButton-panel body[data-theme={theme}]
background-color $ui-dark-noteList-backgroundColor .control-infoButton-panel
background-color get-theme-var(theme, 'noteList-backgroundColor')
.control-infoButton-panel-trash .control-infoButton-panel-trash
background-color $ui-dark-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
.modification-date .modification-date
color $ui-dark-text-color color get-theme-var(theme, '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 $ui-dark-text-color color get-theme-var(theme, 'text-color')
.infoPanel-sub-count .infoPanel-sub-count
color $ui-inactive-text-color color $ui-inactive-text-color
.infoPanel-default .infoPanel-default
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
.infoPanel-sub .infoPanel-sub
color $ui-inactive-text-color color $ui-inactive-text-color
.infoPanel-noteLink .infoPanel-noteLink
background-color alpha($ui-dark-borderColor, 60%) background-color alpha(get-theme-var(theme, 'borderColor'), 20%)
color $ui-dark-text-color color get-theme-var(theme, '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($ui-dark-borderColor, 20%) background-color alpha(get-theme-var(theme, 'borderColor'), 20%)
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
p p
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
&:hover &:hover
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
body[data-theme="solarized-dark"] for theme in 'dark' 'solarized-dark' 'dracula'
.control-infoButton-panel apply-theme(theme)
background-color $ui-solarized-dark-noteList-backgroundColor
.control-infoButton-panel-trash for theme in $themes
background-color $ui-solarized-ark-noteList-backgroundColor apply-theme(theme)
.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
body[data-theme="monokai"]
.control-infoButton-panel
background-color $ui-monokai-noteList-backgroundColor
.control-infoButton-panel-trash
background-color $ui-monokai-noteList-backgroundColor
.modification-date
color $ui-monokai-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-monokai-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-monokai-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-monokai-borderColor, 20%)
color $ui-monokai-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-monokai-borderColor, 20%)
color $ui-monokai-text-color
p
color $ui-dark-inactive-text-color
&:hover
color $ui-monokai-text-color
body[data-theme="dracula"]
.control-infoButton-panel
background-color $ui-dracula-noteList-backgroundColor
.control-infoButton-panel-trash
background-color $ui-dracula-noteList-backgroundColor
.modification-date
color $ui-dracula-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-dracula-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-dracula-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-dracula-borderColor, 20%)
color $ui-dracula-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-dracula-borderColor, 20%)
color $ui-dracula-text-color
p
color $ui-dark-inactive-text-color
&:hover
color $ui-dracula-text-color

View File

@@ -5,9 +5,20 @@ import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const InfoPanelTrashed = ({ const InfoPanelTrashed = ({
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml storageName,
folderName,
updatedAt,
createdAt,
exportAsMd,
exportAsTxt,
exportAsHtml,
exportAsPdf
}) => ( }) => (
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}> <div
className='infoPanel'
styleName='control-infoButton-panel-trash'
style={{ display: 'none' }}
>
<div> <div>
<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'>{i18n.__('MODIFICATION DATE')}</p>
@@ -21,7 +32,10 @@ const InfoPanelTrashed = ({
</div> </div>
<div> <div>
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p> <p styleName='infoPanel-default'>
<text styleName='infoPanel-trash'>Trash</text>
{folderName}
</p>
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p> <p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div> </div>
@@ -31,22 +45,34 @@ const InfoPanelTrashed = ({
</div> </div>
<div id='export-wrap'> <div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}> <button
styleName='export--enable'
onClick={e => exportAsMd(e, 'export-md')}
>
<i className='fa fa-file-code-o' /> <i className='fa fa-file-code-o' />
<p>.md</p> <p>.md</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}> <button
styleName='export--enable'
onClick={e => exportAsTxt(e, 'export-txt')}
>
<i className='fa fa-file-text-o' /> <i className='fa fa-file-text-o' />
<p>.txt</p> <p>.txt</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}> <button
styleName='export--enable'
onClick={e => exportAsHtml(e, 'export-html')}
>
<i className='fa fa-html5' /> <i className='fa fa-html5' />
<p>.html</p> <p>.html</p>
</button> </button>
<button styleName='export--unable'> <button
styleName='export--enable'
onClick={e => exportAsPdf(e, 'export-pdf')}
>
<i className='fa fa-file-pdf-o' /> <i className='fa fa-file-pdf-o' />
<p>.pdf</p> <p>.pdf</p>
</button> </button>
@@ -61,7 +87,8 @@ InfoPanelTrashed.propTypes = {
createdAt: PropTypes.string.isRequired, createdAt: PropTypes.string.isRequired,
exportAsMd: PropTypes.func.isRequired, exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired, exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired exportAsHtml: PropTypes.func.isRequired,
exportAsPdf: PropTypes.func.isRequired
} }
export default CSSModules(InfoPanelTrashed, styles) export default CSSModules(InfoPanelTrashed, styles)

View File

@@ -1,3 +1,4 @@
/* 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'
@@ -9,7 +10,6 @@ import StarButton from './StarButton'
import TagSelect from './TagSelect' import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect' import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import markdown from 'browser/lib/markdownTextHelper' import markdown from 'browser/lib/markdownTextHelper'
import StatusBar from '../StatusBar' import StatusBar from '../StatusBar'
@@ -30,107 +30,146 @@ import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags' import striptags from 'striptags'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import markdownToc from 'browser/lib/markdown-toc-generator' import markdownToc from 'browser/lib/markdown-toc-generator'
import queryString from 'query-string'
import { replace } from 'connected-react-router'
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: '', {
content: '', title: '',
linesHighlighted: [] 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.toggleLockButton = this.handleToggleLockButton.bind(this) this.toggleLockButton = this.handleToggleLockButton.bind(this)
this.generateToc = () => this.handleGenerateToc() this.generateToc = this.handleGenerateToc.bind(this)
this.handleUpdateContent = this.handleUpdateContent.bind(this)
this.handleSwitchStackDirection = this.handleSwitchStackDirection.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', () => { ee.on('topbar:togglemodebutton', () => {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT' const reversedType =
this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType) this.handleSwitchMode(reversedType)
}) })
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this)) ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
ee.on('code:generate-toc', this.generateToc) ee.on('code:generate-toc', this.generateToc)
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
const isNewNote = nextProps.note.key !== this.props.note.key const isNewNote = nextProps.note.key !== this.props.note.key
const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length const hasDeletedTags =
nextProps.note.tags.length < this.props.note.tags.length
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) { if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow() if (this.saveQueue != null) this.saveNow()
this.setState(
{
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({
note: Object.assign({linesHighlighted: []}, nextProps.note) switchPreview
}, () => {
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.off('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', this.handleSwitchDirection)
ee.off('code:generate-toc', this.generateToc) ee.off('code:generate-toc', this.generateToc)
if (this.saveQueue != null) this.saveNow() if (this.saveQueue != null) this.saveNow()
} }
handleUpdateTag () { 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, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
let title = findNoteTitle(
note.content,
this.props.config.editor.enableFrontMatterTitle,
this.props.config.editor.frontMatterTitleField
)
title = striptags(title)
title = markdown.strip(title)
note.title = title
this.updateNote(note) this.updateNote(note)
} }
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 dataApi.updateNote(note.storage, note.key, this.state.note).then(note => {
.updateNote(note.storage, note.key, this.state.note) dispatch({
.then((note) => { type: 'UPDATE_NOTE',
dispatch({ note: note
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('-')
@@ -139,60 +178,71 @@ 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, {
note: Object.assign({}, newNote) isMovingNote: true,
}, () => { note: Object.assign({}, newNote)
const { dispatch, location } = this.props },
dispatch({ () => {
type: 'MOVE_NOTE', const { dispatch, location } = this.props
originNote: note, dispatch({
note: newNote type: 'MOVE_NOTE',
}) originNote: note,
hashHistory.replace({ note: newNote
pathname: location.pathname, })
query: { dispatch(
key: newNote.key replace({
} pathname: location.pathname,
}) search: queryString.stringify({
this.setState({ key: newNote.key
isMovingNote: false })
}) })
}) )
this.setState({
isMovingNote: false
})
}
)
}) })
} }
handleStarButtonClick (e) { handleStarButtonClick(e) {
const { note } = this.state const { note } = this.state
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR') if (!note.isStarred)
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')
} }
handleKeyDown (e) { exportAsPdf() {
ee.emit('export:save-pdf')
}
handleKeyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
// tab key // tab key
case 9: case 9:
@@ -202,7 +252,11 @@ class MarkdownNoteDetail extends React.Component {
} else if (e.ctrlKey && e.shiftKey) { } else if (e.ctrlKey && e.shiftKey) {
e.preventDefault() e.preventDefault()
this.jumpPrevTab() this.jumpPrevTab()
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) { } else if (
!e.ctrlKey &&
!e.shiftKey &&
e.target === this.refs.description
) {
e.preventDefault() e.preventDefault()
this.focusEditor() this.focusEditor()
} }
@@ -210,9 +264,8 @@ class MarkdownNoteDetail extends React.Component {
// I key // I key
case 73: case 73:
{ {
const isSuper = global.process.platform === 'darwin' const isSuper =
? e.metaKey global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey
: e.ctrlKey
if (isSuper) { if (isSuper) {
e.preventDefault() e.preventDefault()
this.handleInfoButtonClick(e) this.handleInfoButtonClick(e)
@@ -222,17 +275,17 @@ class MarkdownNoteDetail extends React.Component {
} }
} }
handleTrashButtonClick (e) { 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.config.ui
if (isTrashed) { if (isTrashed) {
if (confirmDeleteNote(confirmDeletion, true)) { if (confirmDeleteNote(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',
@@ -248,102 +301,139 @@ class MarkdownNoteDetail extends React.Component {
if (confirmDeleteNote(confirmDeletion, false)) { if (confirmDeleteNote(confirmDeletion, false)) {
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() () => {
ee.emit('list:next') this.save()
}) 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 ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg' return this.state.isLocked
? '../resources/icon/icon-lock.svg'
: '../resources/icon/icon-unlock.svg'
} }
handleDeleteKeyDown (e) { handleDeleteKeyDown(e) {
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 (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') { if (noteStatus === 'CODE') {
this.setState({isLockButtonShown: true}) this.setState({ isLockButtonShown: true })
} else { } else {
this.setState({isLockButtonShown: false}) this.setState({ isLockButtonShown: false })
} }
} }
handleGenerateToc () { handleGenerateToc() {
const editor = this.refs.content.refs.code.editor const editor = this.refs.content.refs.code.editor
markdownToc.generateInEditor(editor) markdownToc.generateInEditor(editor)
} }
handleFocus (e) { handleFocus(e) {
this.focus() this.focus()
} }
handleInfoButtonClick (e) { handleInfoButtonClick(e) {
const infoPanel = document.querySelector('.infoPanel') const infoPanel = document.querySelector('.infoPanel')
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none' if (infoPanel.style)
infoPanel.style.display =
infoPanel.style.display === 'none' ? 'inline' : 'none'
} }
print (e) { print(e) {
ee.emit('print') ee.emit('print')
} }
handleSwitchMode (type) { handleSwitchMode(type) {
this.setState({ editorType: type }, () => { // If in split mode, hide the lock button
this.focus() 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)
}
)
} }
handleDeleteNote () { handleSwitchStackDirection() {
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() this.handleTrashButtonClick()
} }
handleClearTodo () { handleClearTodo() {
const { note } = this.state const { note } = this.state
const splitted = note.content.split('\n') const splitted = note.content.split('\n')
const clearTodoContent = splitted.map((line) => { const clearTodoContent = splitted
const trimmedLine = line.trim() .map(line => {
if (trimmedLine.match(/\[x\]/i)) { const trimmedLine = line.trim()
return line.replace(/\[x\]/i, '[ ]') if (trimmedLine.match(/\[x\]/i)) {
} else { return line.replace(/\[x\]/i, '[ ]')
return line } else {
} return line
}).join('\n') }
})
.join('\n')
note.content = clearTodoContent note.content = clearTodoContent
this.refs.content.setValue(note.content) this.refs.content.setValue(note.content)
@@ -351,155 +441,195 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note) this.updateNote(note)
} }
renderEditor () { renderEditor() {
const { config, ignorePreviewPointerEvents } = this.props const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state const { note, isStacking } = this.state
if (this.state.editorType === 'EDITOR_PREVIEW') { if (this.state.editorType === 'EDITOR_PREVIEW') {
return <MarkdownEditor return (
ref='content' <MarkdownEditor
styleName='body-noteEditor' ref='content'
config={config} styleName='body-noteEditor'
value={note.content} config={config}
storageKey={note.storage} value={note.content}
noteKey={note.key} storageKey={note.storage}
linesHighlighted={note.linesHighlighted} noteKey={note.key}
onChange={this.handleUpdateContent.bind(this)} linesHighlighted={note.linesHighlighted}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} onChange={this.handleUpdateContent}
/> isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={config.editor.rtlEnabled && this.state.RTL}
/>
)
} else { } else {
return <MarkdownSplitEditor return (
ref='content' <MarkdownSplitEditor
config={config} ref='content'
value={note.content} config={config}
storageKey={note.storage} value={note.content}
noteKey={note.key} storageKey={note.storage}
linesHighlighted={note.linesHighlighted} noteKey={note.key}
onChange={this.handleUpdateContent.bind(this)} isStacking={isStacking}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} linesHighlighted={note.linesHighlighted}
/> onChange={this.handleUpdateContent}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={config.editor.rtlEnabled && this.state.RTL}
/>
)
} }
} }
render () { render() {
const { data, location, config } = this.props const { data, dispatch, location, config } = this.props
const { note, editorType } = this.state const { note, editorType } = this.state
const storageKey = note.storage const storageKey = note.storage
const folderKey = note.folder const folderKey = note.folder
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 trashTopBar = <div styleName='info'> const currentOption = _.find(
<div styleName='info-left'> options,
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} /> option =>
</div> option.storage.key === storageKey && option.folder.key === folderKey
<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 = <div styleName='info'> // currentOption may be undefined
<div styleName='info-left'> const storageName = _.get(currentOption, 'storage.name') || ''
<div styleName='info-left-top'> const folderName = _.get(currentOption, 'folder.name') || ''
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder} const trashTopBar = (
ref='folder' <div styleName='info'>
data={data} <div styleName='info-left'>
onChange={(e) => this.handleFolderChange(e)} <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>
<TagSelect
ref='tags'
value={this.state.note.tags}
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
onChange={this.handleUpdateTag.bind(this)}
/>
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div> </div>
<div styleName='info-right'> )
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
{(() => { const detailTopBar = (
const imgSrc = `${this.getToggleLockButton()}` <div styleName='info'>
const lockButtonComponent = <div styleName='info-left'>
<button styleName='control-lockButton' <div>
onFocus={(e) => this.handleFocus(e)} <FolderSelect
onMouseDown={(e) => this.handleLockButtonMouseDown(e)} styleName='info-left-top-folderSelect'
> value={this.state.note.storage + '-' + this.state.note.folder}
<img styleName='iconInfo' src={imgSrc} /> ref='folder'
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>} data={data}
</button> onChange={e => this.handleFolderChange(e)}
/>
</div>
return ( <TagSelect
this.state.isLockButtonShown ? lockButtonComponent : '' ref='tags'
) value={this.state.note.tags}
})()} saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
dispatch={dispatch}
onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags}
/>
<TodoListPercentage
onClearCheckboxClick={e => this.handleClearTodo(e)}
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
/>
</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}
/>
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} /> {(() => {
const imgSrc = `${this.getToggleLockButton()}`
const lockButtonComponent = (
<button
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>
)
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} /> return this.state.isLockButtonShown ? lockButtonComponent : ''
})()}
<InfoButton <FullscreenButton onClick={e => this.handleFullScreenButton(e)} />
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanel <TrashButton onClick={e => this.handleTrashButtonClick(e)} />
storageName={currentOption.storage.name}
folderName={currentOption.folder.name} <InfoButton onClick={e => this.handleInfoButtonClick(e)} />
noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)} <InfoPanel
createdAt={formatDate(note.createdAt)} storageName={storageName}
exportAsMd={this.exportAsMd} folderName={folderName}
exportAsTxt={this.exportAsTxt} noteLink={`[${note.title}](:note:${
exportAsHtml={this.exportAsHtml} queryString.parse(location.search).key
wordCount={note.content.split(' ').length} })`}
letterCount={note.content.replace(/\r?\n/g, '').length} updatedAt={formatDate(note.updatedAt)}
type={note.type} createdAt={formatDate(note.createdAt)}
print={this.print} 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> )
return ( return (
<div className='NoteDetail' <div
className='NoteDetail'
style={this.props.style} style={this.props.style}
styleName='root' styleName='root'
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={e => this.handleKeyDown(e)}
> >
{location.pathname === '/trashed' ? trashTopBar : detailTopBar} {location.pathname === '/trashed' ? trashTopBar : detailTopBar}
<div styleName='body'> <div styleName='body'>{this.renderEditor()}</div>
{this.renderEditor()}
</div>
<StatusBar <StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])} {..._.pick(this.props, ['config', 'location', 'dispatch'])}
@@ -513,9 +643,7 @@ 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
}), }),

View File

@@ -15,7 +15,7 @@
.control-lockButton .control-lockButton
topBarButtonRight() topBarButtonRight()
position absolute position absolute
right 225px right 265px
&:hover .tooltip &:hover .tooltip
opacity 1 opacity 1
@@ -66,18 +66,14 @@ 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')
body[data-theme="solarized-dark"] for theme in 'solarized-dark' 'dracula'
.root apply-theme(theme)
border-left 1px solid $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor
body[data-theme="monokai"] for theme in $themes
.root apply-theme(theme)
border-left 1px solid $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
body[data-theme="dracula"]
.root
border-left 1px solid $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor

View File

@@ -15,6 +15,14 @@ $info-margin-under-border = 30px
padding 0 20px padding 0 20px
z-index 99 z-index 99
.info > div
> button
-webkit-user-drag none
user-select none
> img, span
-webkit-user-drag none
user-select none
.info-left .info-left
padding 0 10px padding 0 10px
width 100% width 100%
@@ -94,17 +102,14 @@ body[data-theme="dark"]
.undo-button .undo-button
topBarButtonDark() topBarButtonDark()
body[data-theme="solarized-dark"] apply-theme(theme)
.info body[data-theme={theme}]
border-color $ui-solarized-dark-borderColor .info
background-color $ui-solarized-dark-noteDetail-backgroundColor border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.info apply-theme(theme)
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
body[data-theme="dracula"] for theme in $themes
.info apply-theme(theme)
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor

View File

@@ -4,13 +4,9 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl' import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const PermanentDeleteButton = ({ const PermanentDeleteButton = ({ onClick }) => (
onClick <button styleName='control-trashButton--in-trash' onClick={e => onClick(e)}>
}) => ( <img src='../resources/icon/icon-trash.svg' />
<button styleName='control-trashButton--in-trash'
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span> <span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button> </button>
) )

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './RestoreButton.styl' import styles from './RestoreButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const RestoreButton = ({ const RestoreButton = ({ onClick }) => (
onClick <button styleName='control-restoreButton' onClick={onClick}>
}) => (
<button styleName='control-restoreButton'
onClick={onClick}
>
<i className='fa fa-undo fa-fw' styleName='iconRestore' /> <i className='fa fa-undo fa-fw' styleName='iconRestore' />
<span styleName='tooltip'>{i18n.__('Restore')}</span> <span styleName='tooltip'>{i18n.__('Restore')}</span>
</button> </button>

File diff suppressed because it is too large Load Diff

View File

@@ -156,78 +156,35 @@ body[data-theme="dark"]
.control-fullScreenButton .control-fullScreenButton
topBarButtonDark() topBarButtonDark()
body[data-theme="solarized-dark"] apply-theme(theme)
.root body[data-theme={theme}]
border-left 1px solid $ui-solarized-dark-borderColor .root
background-color $ui-solarized-dark-noteDetail-backgroundColor border-left 1px solid get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
.body .body
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
.body .description textarea .body .description textarea
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
border 1px solid $ui-solarized-dark-borderColor border 1px solid get-theme-var(theme, 'borderColor')
.tabList .tabButton .tabList .tabButton
border-color $ui-solarized-dark-borderColor border-color get-theme-var(theme, 'borderColor')
.tabButton .tabButton
&:hover &:hover
color $ui-solarized-dark-button--active-color color get-theme-var(theme, 'text-color')
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
transition 0.15s transition 0.15s
.tabList
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color
body[data-theme="monokai"] .tabList
.root background-color get-theme-var(theme, 'noteDetail-backgroundColor')
border-left 1px solid $ui-monokai-borderColor color get-theme-var(theme, 'text-color')
background-color $ui-monokai-noteDetail-backgroundColor
.body for theme in 'solarized-dark' 'dracula'
background-color $ui-monokai-noteDetail-backgroundColor apply-theme(theme)
.body .description textarea for theme in $themes
background-color $ui-monokai-noteDetail-backgroundColor apply-theme(theme)
color $ui-monokai-text-color
border 1px solid $ui-monokai-borderColor
.tabList .tabButton
border-color $ui-monokai-borderColor
.tabButton
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-noteDetail-backgroundColor
.tabList
background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color
body[data-theme="dracula"]
.root
border-left 1px solid $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor
.body
background-color $ui-dracula-noteDetail-backgroundColor
.body .description textarea
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color
border 1px solid $ui-dracula-borderColor
.tabList .tabButton
border-color $ui-dracula-borderColor
.tabButton
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-noteDetail-backgroundColor
.tabList
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color

View File

@@ -6,7 +6,7 @@ import _ from 'lodash'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
class StarButton extends React.Component { class StarButton extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@@ -14,47 +14,51 @@ class StarButton extends React.Component {
} }
} }
handleMouseDown (e) { handleMouseDown(e) {
this.setState({ this.setState({
isActive: true isActive: true
}) })
} }
handleMouseUp (e) { handleMouseUp(e) {
this.setState({ this.setState({
isActive: false isActive: false
}) })
} }
handleMouseLeave (e) { handleMouseLeave(e) {
this.setState({ this.setState({
isActive: false isActive: false
}) })
} }
render () { render() {
const { className } = this.props const { className } = this.props
return ( return (
<button className={_.isString(className) <button
? 'StarButton ' + className className={
: 'StarButton' _.isString(className) ? 'StarButton ' + className : 'StarButton'
} }
styleName={this.state.isActive || this.props.isActive styleName={
? 'root--active' this.state.isActive || this.props.isActive ? 'root--active' : 'root'
: 'root'
} }
onMouseDown={(e) => this.handleMouseDown(e)} onMouseDown={e => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)} onMouseUp={e => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)} onMouseLeave={e => this.handleMouseLeave(e)}
onClick={this.props.onClick}> onClick={this.props.onClick}
<img styleName='icon' >
src={this.state.isActive || this.props.isActive <img
? '../resources/icon/icon-starred.svg' styleName='icon'
: '../resources/icon/icon-star.svg' src={
this.state.isActive || this.props.isActive
? '../resources/icon/icon-starred.svg'
: '../resources/icon/icon-star.svg'
} }
/> />
<span styleName='tooltip'>{i18n.__('Star')}</span> <span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Star')}
</span>
</button> </button>
) )
} }

View File

@@ -21,6 +21,11 @@
opacity 0 opacity 0
transition 0.1s transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
right 103px
width 70px
.root--active .root--active
@extend .root @extend .root
transition 0.15s transition 0.15s
@@ -37,4 +42,4 @@ body[data-theme="dark"]
topBarButtonDark() topBarButtonDark()
&:hover &:hover
transition 0.2s transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6) color alpha($ui-favorite-star-button-color, 0.6)

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import invertColor from 'invert-color'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl' import styles from './TagSelect.styl'
import _ from 'lodash' import _ from 'lodash'
@@ -7,9 +8,10 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import Autosuggest from 'react-autosuggest' import Autosuggest from 'react-autosuggest'
import { push } from 'connected-react-router'
class TagSelect extends React.Component { class TagSelect extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@@ -18,15 +20,20 @@ class TagSelect extends React.Component {
} }
this.handleAddTag = this.handleAddTag.bind(this) this.handleAddTag = this.handleAddTag.bind(this)
this.handleRenameTag = this.handleRenameTag.bind(this)
this.onInputBlur = this.onInputBlur.bind(this) this.onInputBlur = this.onInputBlur.bind(this)
this.onInputChange = this.onInputChange.bind(this) this.onInputChange = this.onInputChange.bind(this)
this.onInputKeyDown = this.onInputKeyDown.bind(this) this.onInputKeyDown = this.onInputKeyDown.bind(this)
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this) this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this) this
)
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(
this
)
this.onSuggestionSelected = this.onSuggestionSelected.bind(this) this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
} }
addNewTag (newTag) { addNewTag(newTag) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
newTag = newTag.trim().replace(/ +/g, '_') newTag = newTag.trim().replace(/ +/g, '_')
@@ -42,9 +49,7 @@ class TagSelect extends React.Component {
} }
let { value } = this.props let { value } = this.props
value = _.isArray(value) value = _.isArray(value) ? value.slice() : []
? value.slice()
: []
if (!_.includes(value, newTag)) { if (!_.includes(value, newTag)) {
value.push(newTag) value.push(newTag)
@@ -54,68 +59,87 @@ class TagSelect extends React.Component {
value = _.sortBy(value) value = _.sortBy(value)
} }
this.setState({ this.setState(
newTag: '' {
}, () => { newTag: ''
this.value = value },
this.props.onChange() () => {
}) this.value = value
this.props.onChange()
}
)
} }
buildSuggestions () { buildSuggestions() {
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map( this.suggestions = _.sortBy(
(tag, name) => ({ this.props.data.tagNoteMap
name, .map((tag, name) => ({
nameLC: name.toLowerCase(), name,
size: tag.size nameLC: name.toLowerCase(),
}) size: tag.size
).filter( }))
tag => tag.size > 0 .filter(tag => tag.size > 0),
), ['name']) ['name']
)
} }
componentDidMount () { componentDidMount() {
this.value = this.props.value this.value = this.props.value
this.buildSuggestions() this.buildSuggestions()
ee.on('editor:add-tag', this.handleAddTag) ee.on('editor:add-tag', this.handleAddTag)
ee.on('sidebar:rename-tag', this.handleRenameTag)
} }
componentDidUpdate () { componentDidUpdate() {
this.value = this.props.value this.value = this.props.value
} }
componentWillUnmount () { componentWillUnmount() {
ee.off('editor:add-tag', this.handleAddTag) ee.off('editor:add-tag', this.handleAddTag)
ee.off('sidebar:rename-tag', this.handleRenameTag)
} }
handleAddTag () { handleAddTag() {
this.refs.newTag.input.focus() this.refs.newTag.input.focus()
} }
handleTagLabelClick (tag) { handleRenameTag(event, tagChange) {
const { router } = this.context const { value } = this.props
router.push(`/tags/${tag}`) const { tag, updatedTag } = tagChange
const newTags = value.slice()
newTags[value.indexOf(tag)] = updatedTag
this.value = newTags
this.props.onChange()
} }
handleTagRemoveButtonClick (tag) { handleTagLabelClick(tag) {
const { dispatch } = this.props
// Note: `tag` requires encoding later.
// E.g. % in tag is a problem (see issue #3170) - encodeURIComponent(tag) is not working.
dispatch(push(`/tags/${tag}`))
}
handleTagRemoveButtonClick(tag) {
this.removeTagByCallback((value, tag) => { this.removeTagByCallback((value, tag) => {
value.splice(value.indexOf(tag), 1) value.splice(value.indexOf(tag), 1)
}, tag) }, tag)
} }
onInputBlur (e) { onInputBlur(e) {
this.submitNewTag() this.submitNewTag()
} }
onInputChange (e, { newValue, method }) { onInputChange(e, { newValue, method }) {
this.setState({ this.setState({
newTag: newValue newTag: newValue
}) })
} }
onInputKeyDown (e) { onInputKeyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
case 9: case 9:
e.preventDefault() e.preventDefault()
@@ -131,17 +155,18 @@ class TagSelect extends React.Component {
} }
} }
onSuggestionsClearRequested () { onSuggestionsClearRequested() {
this.setState({ this.setState({
suggestions: [] suggestions: []
}) })
} }
onSuggestionsFetchRequested ({ value }) { onSuggestionsFetchRequested({ value }) {
const valueLC = value.toLowerCase() const valueLC = value.toLowerCase()
const suggestions = _.filter( const suggestions = _.filter(
this.suggestions, this.suggestions,
tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1 tag =>
!_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
) )
this.setState({ this.setState({
@@ -149,22 +174,20 @@ class TagSelect extends React.Component {
}) })
} }
onSuggestionSelected (event, { suggestion, suggestionValue }) { onSuggestionSelected(event, { suggestion, suggestionValue }) {
this.addNewTag(suggestionValue) this.addNewTag(suggestionValue)
} }
removeLastTag () { removeLastTag() {
this.removeTagByCallback((value) => { this.removeTagByCallback(value => {
value.pop() value.pop()
}) })
} }
removeTagByCallback (callback, tag = null) { removeTagByCallback(callback, tag = null) {
let { value } = this.props let { value } = this.props
value = _.isArray(value) value = _.isArray(value) ? value.slice() : []
? value.slice()
: []
callback(value, tag) callback(value, tag)
value = _.uniq(value) value = _.uniq(value)
@@ -172,7 +195,7 @@ class TagSelect extends React.Component {
this.props.onChange() this.props.onChange()
} }
reset () { reset() {
this.buildSuggestions() this.buildSuggestions()
this.setState({ this.setState({
@@ -180,36 +203,60 @@ class TagSelect extends React.Component {
}) })
} }
submitNewTag () { submitNewTag() {
this.addNewTag(this.refs.newTag.input.value) this.addNewTag(this.refs.newTag.input.value)
} }
render () { render() {
const { value, className, showTagsAlphabetically } = this.props const { value, className, showTagsAlphabetically, coloredTags } = this.props
const tagList = _.isArray(value) const tagList = _.isArray(value)
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => { ? (showTagsAlphabetically ? _.sortBy(value) : value).map(tag => {
return ( const wrapperStyle = {}
<span styleName='tag' const textStyle = {}
key={tag} const BLACK = '#333333'
> const WHITE = '#f1f1f1'
<span styleName='tag-label' onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span> const color = coloredTags[tag]
<button styleName='tag-removeButton' const invertedColor =
onClick={(e) => this.handleTagRemoveButtonClick(tag)} color && invertColor(color, { black: BLACK, white: WHITE })
> let iconRemove = '../resources/icon/icon-x.svg'
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' /> if (color) {
</button> wrapperStyle.backgroundColor = color
</span> textStyle.color = invertedColor
) }
}) if (invertedColor === WHITE) {
iconRemove = '../resources/icon/icon-x-light.svg'
}
return (
<span styleName='tag' key={tag} style={wrapperStyle}>
<span
styleName='tag-label'
style={textStyle}
onClick={e => this.handleTagLabelClick(tag)}
>
#{tag}
</span>
<button
styleName='tag-removeButton'
onClick={e => this.handleTagRemoveButtonClick(tag)}
>
<img
className='tag-removeButton-icon'
src={iconRemove}
width='8px'
/>
</button>
</span>
)
})
: [] : []
const { newTag, suggestions } = this.state const { newTag, suggestions } = this.state
return ( return (
<div className={_.isString(className) <div
? 'TagSelect ' + className className={
: 'TagSelect' _.isString(className) ? 'TagSelect ' + className : 'TagSelect'
} }
styleName='root' styleName='root'
> >
@@ -221,11 +268,7 @@ class TagSelect extends React.Component {
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected} onSuggestionSelected={this.onSuggestionSelected}
getSuggestionValue={suggestion => suggestion.name} getSuggestionValue={suggestion => suggestion.name}
renderSuggestion={suggestion => ( renderSuggestion={suggestion => <div>{suggestion.name}</div>}
<div>
{suggestion.name}
</div>
)}
inputProps={{ inputProps={{
placeholder: i18n.__('Add tag...'), placeholder: i18n.__('Add tag...'),
value: newTag, value: newTag,
@@ -239,14 +282,12 @@ class TagSelect extends React.Component {
} }
} }
TagSelect.contextTypes = {
router: PropTypes.shape({})
}
TagSelect.propTypes = { TagSelect.propTypes = {
dispatch: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string), value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func onChange: PropTypes.func,
coloredTags: PropTypes.object
} }
export default CSSModules(TagSelect, styles) export default CSSModules(TagSelect, styles)

View File

@@ -3,19 +3,18 @@
align-items center align-items center
user-select none user-select none
vertical-align middle vertical-align middle
width 100% width 96%
overflow-x scroll overflow-x auto
white-space nowrap white-space nowrap
margin-top 31px top 50px
position absolute position absolute
&::-webkit-scrollbar
.root::-webkit-scrollbar height 8px
display none
.tag .tag
display flex display flex
align-items center align-items center
margin 0px 2px margin 0px 2px 2px
padding 2px 4px padding 2px 4px
background-color alpha($ui-tag-backgroundColor, 3%) background-color alpha($ui-tag-backgroundColor, 3%)
border-radius 4px border-radius 4px
@@ -55,35 +54,20 @@ body[data-theme="dark"]
.tag-label .tag-label
color $ui-dark-text-color color $ui-dark-text-color
body[data-theme="solarized-dark"] apply-theme(theme)
.tag body[data-theme={theme}]
background-color $ui-solarized-dark-tag-backgroundColor .tag
background-color get-theme-var(theme, 'tag-backgroundColor')
.tag-removeButton .tag-removeButton
border-color $ui-button--focus-borderColor border-color $ui-button--focus-borderColor
background-color transparent background-color transparent
.tag-label .tag-label
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.tag apply-theme(theme)
background-color $ui-monokai-tag-backgroundColor
.tag-removeButton for theme in $themes
border-color $ui-button--focus-borderColor apply-theme(theme)
background-color transparent
.tag-label
color $ui-monokai-text-color
body[data-theme="dracula"]
.tag
background-color $ui-dracula-tag-backgroundColor
.tag-removeButton
border-color $ui-dracula-button--focus-borderColor
background-color transparent
.tag-label
color $ui-dracula-borderColor

View File

@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleDirectionButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleDirectionButton = ({ onClick, isRTL }) => (
<div styleName='control-toggleModeButton'>
<div styleName={isRTL ? 'active' : undefined} onClick={() => onClick()}>
<img src={!isRTL ? '../resources/icon/icon-left-to-right.svg' : ''} />
</div>
<div styleName={!isRTL ? 'active' : undefined} onClick={() => onClick()}>
<img src={!isRTL ? '' : '../resources/icon/icon-right-to-left.svg'} />
</div>
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Toggle Direction')}
</span>
</div>
)
ToggleDirectionButton.propTypes = {
onClick: PropTypes.func.isRequired,
isRTL: PropTypes.bool.isRequired
}
export default CSSModules(ToggleDirectionButton, styles)

View File

@@ -0,0 +1,85 @@
.control-toggleModeButton
height 25px
border-radius 50px
background-color #F4F4F4
width 52px
display flex
align-items center
position: relative
top 2px
margin-left 5px
.active
background-color #1EC38B
width 33px
height 24px
box-shadow 2px 0px 7px #eee
z-index 1
div
width 40px
height 100%
border-radius 50%
display flex
align-items center
justify-content center
cursor pointer
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 33px
left -10px
z-index 200
width 80px
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
left -8px
width 70px
.control-toggleModeButton
-webkit-user-drag none
user-select none
> div img
-webkit-user-drag none
user-select none
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
.control-toggleModeButton
background-color #3A404C
.active
background-color #1EC38B
box-shadow 2px 0px 7px #444444
body[data-theme="solarized-dark"]
.control-toggleModeButton
background-color #002B36
.active
background-color #1EC38B
box-shadow 2px 0px 7px #222222
apply-theme(theme)
body[data-theme={theme}]
.control-toggleModeButton
background-color get-theme-var(theme, 'borderColor')
.active
background-color get-theme-var(theme, 'active-color')
box-shadow 2px 0px 7px #222222
for theme in 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

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