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

Merge pull request #1 from BoostIO/master

Merge updated
This commit is contained in:
Antogin
2018-09-07 11:17:03 +02:00
committed by GitHub
250 changed files with 15346 additions and 5098 deletions

View File

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

View File

@@ -10,7 +10,6 @@
"theme": "monokai" "theme": "monokai"
}, },
"hotkey": { "hotkey": {
"toggleFinder": "Cmd + Alt + S",
"toggleMain": "Cmd + Alt + L" "toggleMain": "Cmd + Alt + L"
}, },
"isSideNavFolded": false, "isSideNavFolded": false,
@@ -23,7 +22,10 @@
"fontSize": "14", "fontSize": "14",
"lineNumber": true "lineNumber": true
}, },
"sortBy": "UPDATED_AT", "sortBy": {
"default": "UPDATED_AT"
},
"sortTagsBy": "ALPHABETICAL",
"ui": { "ui": {
"defaultNote": "ALWAYS_ASK", "defaultNote": "ALWAYS_ASK",
"disableDirectWrite": false, "disableDirectWrite": false,

16
.editorconfig Normal file
View File

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

View File

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

View File

@@ -3,7 +3,9 @@
"plugins": ["react"], "plugins": ["react"],
"rules": { "rules": {
"no-useless-escape": 0, "no-useless-escape": 0,
"prefer-const": "warn", "prefer-const": ["warn", {
"destructuring": "all"
}],
"no-unused-vars": "warn", "no-unused-vars": "warn",
"no-undef": "warn", "no-undef": "warn",
"no-lone-blocks": "warn", "no-lone-blocks": "warn",
@@ -17,5 +19,8 @@
"FileReader": true, "FileReader": true,
"localStorage": true, "localStorage": true,
"fetch": true "fetch": true
},
"env": {
"jest": true
} }
} }

View File

@@ -1,9 +1,9 @@
language: node_js language: node_js
node_js: node_js:
- stable - 7
- lts/*
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@5.2 && 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

View File

@@ -1,10 +1,25 @@
# Current behavior
<!-- <!--
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug. Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile. If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
--> -->
# Expected behavior
# Steps to reproduce
1.
2.
3.
# Environment
- Version :
- OS Version and name :
<!-- <!--
Love Boostnote? Please consider supporting us via OpenCollective: Love Boostnote? Please consider supporting us on IssueHunt:
👉 https://opencollective.com/boostnoteio 👉 https://issuehunt.io/repos/53266139
--> -->

View File

@@ -2,7 +2,7 @@ GPL-3.0
Boostnote - an open source note-taking app made for programmers just like you. Boostnote - an open source note-taking app made for programmers just like you.
Copyright (C) 2017 Maisin&Co., Inc. Copyright (C) 2017 - 2018 BoostIO
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by

7
__mocks__/electron.js Normal file
View File

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

View File

@@ -3,36 +3,37 @@ import React from 'react'
import _ from 'lodash' import _ from 'lodash'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir' import 'codemirror-mode-elixir'
import path from 'path' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import copyImage from 'browser/main/lib/dataApi/copyImage' import convertModeName from 'browser/lib/convertModeName'
import { findStorage } from 'browser/lib/findStorage' import { options, TableEditor } from '@susisu/mte-kernel'
import TextEditorInterface from 'browser/lib/TextEditorInterface'
import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
import crypto from 'crypto'
import consts from 'browser/lib/consts'
import fs from 'fs' import fs from 'fs'
const { ipcRenderer } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] const buildCMRulers = (rulers, enableRulers) =>
(enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
function pass (name) {
switch (name) {
case 'ejs':
return 'Embedded Javascript'
case 'html_ruby':
return 'Embedded Ruby'
case 'objectivec':
return 'Objective C'
case 'text':
return 'Plain Text'
default:
return name
}
}
export default class CodeEditor extends React.Component { export default class CodeEditor extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.changeHandler = (e) => this.handleChange(e) this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
leading: false,
trailing: true
})
this.changeHandler = e => this.handleChange(e)
this.focusHandler = () => {
ipcRenderer.send('editor:focused', true)
}
this.blurHandler = (editor, e) => { this.blurHandler = (editor, e) => {
ipcRenderer.send('editor:focused', false)
if (e == null) return null if (e == null) return null
let el = e.relatedTarget let el = e.relatedTarget
while (el != null) { while (el != null) {
@@ -42,18 +43,87 @@ export default class CodeEditor extends React.Component {
el = el.parentNode el = el.parentNode
} }
this.props.onBlur != null && this.props.onBlur(e) this.props.onBlur != null && this.props.onBlur(e)
const { storageKey, noteKey } = this.props
attachmentManagement.deleteAttachmentsNotPresentInNote(
this.editor.getValue(),
storageKey,
noteKey
)
} }
this.pasteHandler = (editor, e) => this.handlePaste(editor, e) this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
this.loadStyleHandler = (e) => { this.loadStyleHandler = e => {
this.editor.refresh() this.editor.refresh()
} }
this.searchHandler = (e, msg) => this.handleSearch(msg)
this.searchState = null
this.formatTable = () => this.handleFormatTable()
}
handleSearch (msg) {
const cm = this.editor
const component = this
if (component.searchState) cm.removeOverlay(component.searchState)
if (msg.length < 3) return
cm.operation(function () {
component.searchState = makeOverlay(msg, 'searching')
cm.addOverlay(component.searchState)
function makeOverlay (query, style) {
query = new RegExp(
query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
'gi'
)
return {
token: function (stream) {
query.lastIndex = stream.pos
var match = query.exec(stream.string)
if (match && match.index === stream.pos) {
stream.pos += match[0].length || 1
return style
} else if (match) {
stream.pos = match.index
} else {
stream.skipToEnd()
}
}
}
}
})
}
handleFormatTable () {
this.tableEditor.formatAll(options({textWidthOptions: {}}))
} }
componentDidMount () { componentDidMount () {
const { rulers, enableRulers } = this.props
const expandSnippet = this.expandSnippet.bind(this)
const 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.'
}
]
if (!fs.existsSync(consts.SNIPPET_FILE)) {
fs.writeFileSync(
consts.SNIPPET_FILE,
JSON.stringify(defaultSnippet, null, 4),
'utf8'
)
}
this.value = this.props.value this.value = this.props.value
this.editor = CodeMirror(this.refs.root, { this.editor = CodeMirror(this.refs.root, {
rulers: buildCMRulers(rulers, enableRulers),
value: this.props.value, value: this.props.value,
lineNumbers: true, lineNumbers: this.props.displayLineNumbers,
lineWrapping: true, lineWrapping: true,
theme: this.props.theme, theme: this.props.theme,
indentUnit: this.props.indentSize, indentUnit: this.props.indentSize,
@@ -63,15 +133,24 @@ export default class CodeEditor extends React.Component {
scrollPastEnd: this.props.scrollPastEnd, scrollPastEnd: this.props.scrollPastEnd,
inputStyle: 'textarea', inputStyle: 'textarea',
dragDrop: false, dragDrop: false,
autoCloseBrackets: true, foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: {
pairs: '()[]{}\'\'""$$**``',
triples: '```"""\'\'\'',
explode: '[]{}``$$',
override: true
},
extraKeys: { extraKeys: {
Tab: function (cm) { Tab: function (cm) {
const cursor = cm.getCursor() const cursor = cm.getCursor()
const line = cm.getLine(cursor.line) const line = cm.getLine(cursor.line)
const cursorPosition = cursor.ch
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
if (cm.somethingSelected()) cm.indentSelection('add') if (cm.somethingSelected()) cm.indentSelection('add')
else { else {
const tabs = cm.getOption('indentWithTabs') const tabs = cm.getOption('indentWithTabs')
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)\] )?$/)) { if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
cm.execCommand('goLineStart') cm.execCommand('goLineStart')
if (tabs) { if (tabs) {
cm.execCommand('insertTab') cm.execCommand('insertTab')
@@ -79,6 +158,21 @@ export default class CodeEditor extends React.Component {
cm.execCommand('insertSoftTab') cm.execCommand('insertSoftTab')
} }
cm.execCommand('goLineEnd') cm.execCommand('goLineEnd')
} else if (
!charBeforeCursor.match(/\t|\s|\r|\n/) &&
cursor.ch > 1
) {
// text expansion on tab key if the char before is alphabet
const snippets = JSON.parse(
fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
)
if (expandSnippet(line, cursor, cm, snippets) === false) {
if (tabs) {
cm.execCommand('insertTab')
} else {
cm.execCommand('insertSoftTab')
}
}
} else { } else {
if (tabs) { if (tabs) {
cm.execCommand('insertTab') cm.execCommand('insertTab')
@@ -91,8 +185,8 @@ export default class CodeEditor extends React.Component {
'Cmd-T': function (cm) { 'Cmd-T': function (cm) {
// Do nothing // Do nothing
}, },
Enter: 'newlineAndIndentContinueMarkdownList', Enter: 'boostNewLineAndIndentContinueMarkdownList',
'Ctrl-C': (cm) => { 'Ctrl-C': cm => {
if (cm.getOption('keyMap').substr(0, 3) === 'vim') { if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
document.execCommand('copy') document.execCommand('copy')
} }
@@ -103,9 +197,14 @@ export default class CodeEditor extends React.Component {
this.setMode(this.props.mode) this.setMode(this.props.mode)
this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler) this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler) this.editor.on('change', this.changeHandler)
this.editor.on('paste', this.pasteHandler) this.editor.on('paste', this.pasteHandler)
eventEmitter.on('top:search', this.searchHandler)
eventEmitter.emit('code:init')
this.editor.on('scroll', this.scrollHandler)
const editorTheme = document.getElementById('editorTheme') const editorTheme = document.getElementById('editorTheme')
editorTheme.addEventListener('load', this.loadStyleHandler) editorTheme.addEventListener('load', this.loadStyleHandler)
@@ -115,6 +214,83 @@ export default class CodeEditor extends React.Component {
CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor) CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor)
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor) CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
CodeMirror.Vim.map('ZZ', ':q', 'normal') CodeMirror.Vim.map('ZZ', ':q', 'normal')
this.tableEditor = new TableEditor(new TextEditorInterface(this.editor))
eventEmitter.on('code:format-table', this.formatTable)
}
expandSnippet (line, cursor, cm, snippets) {
const wordBeforeCursor = this.getWordBeforeCursor(
line,
cursor.line,
cursor.ch
)
const templateCursorString = ':{}'
for (let i = 0; i < snippets.length; i++) {
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
if (snippets[i].content.indexOf(templateCursorString) !== -1) {
const snippetLines = snippets[i].content.split('\n')
let cursorLineNumber = 0
let cursorLinePosition = 0
for (let j = 0; j < snippetLines.length; j++) {
const cursorIndex = snippetLines[j].indexOf(templateCursorString)
if (cursorIndex !== -1) {
cursorLineNumber = j
cursorLinePosition = cursorIndex
cm.replaceRange(
snippets[i].content.replace(templateCursorString, ''),
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
)
cm.setCursor({
line: cursor.line + cursorLineNumber,
ch: cursorLinePosition
})
}
}
} else {
cm.replaceRange(
snippets[i].content,
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
)
}
return true
}
}
return false
}
getWordBeforeCursor (line, lineNumber, cursorPosition) {
let wordBeforeCursor = ''
const originCursorPosition = cursorPosition
const emptyChars = /\t|\s|\r|\n/
// to prevent the word to expand is long that will crash the whole app
// the safeStop is there to stop user to expand words that longer than 20 chars
const safeStop = 20
while (cursorPosition > 0) {
const currentChar = line.substr(cursorPosition - 1, 1)
// if char is not an empty char
if (!emptyChars.test(currentChar)) {
wordBeforeCursor = currentChar + wordBeforeCursor
} else if (wordBeforeCursor.length >= safeStop) {
throw new Error('Your snippet trigger is too long !')
} else {
break
}
cursorPosition--
}
return {
text: wordBeforeCursor,
range: {
from: { line: lineNumber, ch: originCursorPosition },
to: { line: lineNumber, ch: cursorPosition }
}
}
} }
quitEditor () { quitEditor () {
@@ -122,15 +298,21 @@ export default class CodeEditor extends React.Component {
} }
componentWillUnmount () { componentWillUnmount () {
this.editor.off('focus', this.focusHandler)
this.editor.off('blur', this.blurHandler) this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler) this.editor.off('change', this.changeHandler)
this.editor.off('paste', this.pasteHandler) this.editor.off('paste', this.pasteHandler)
eventEmitter.off('top:search', this.searchHandler)
this.editor.off('scroll', this.scrollHandler)
const editorTheme = document.getElementById('editorTheme') const editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler) editorTheme.removeEventListener('load', this.loadStyleHandler)
eventEmitter.off('code:format-table', this.formatTable)
} }
componentDidUpdate (prevProps, prevState) { componentDidUpdate (prevProps, prevState) {
let needRefresh = false let needRefresh = false
const { rulers, enableRulers } = this.props
if (prevProps.mode !== this.props.mode) { if (prevProps.mode !== this.props.mode) {
this.setMode(this.props.mode) this.setMode(this.props.mode)
} }
@@ -148,6 +330,13 @@ export default class CodeEditor extends React.Component {
needRefresh = true needRefresh = true
} }
if (
prevProps.enableRulers !== enableRulers ||
prevProps.rulers !== rulers
) {
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
}
if (prevProps.indentSize !== this.props.indentSize) { if (prevProps.indentSize !== this.props.indentSize) {
this.editor.setOption('indentUnit', this.props.indentSize) this.editor.setOption('indentUnit', this.props.indentSize)
this.editor.setOption('tabSize', this.props.indentSize) this.editor.setOption('tabSize', this.props.indentSize)
@@ -156,6 +345,10 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space') this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
} }
if (prevProps.displayLineNumbers !== this.props.displayLineNumbers) {
this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
}
if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) { if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
} }
@@ -166,7 +359,7 @@ export default class CodeEditor extends React.Component {
} }
setMode (mode) { setMode (mode) {
let syntax = CodeMirror.findModeByName(pass(mode)) let syntax = CodeMirror.findModeByName(convertModeName(mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
this.editor.setOption('mode', syntax.mime) this.editor.setOption('mode', syntax.mime)
@@ -180,11 +373,9 @@ export default class CodeEditor extends React.Component {
} }
} }
moveCursorTo (row, col) { moveCursorTo (row, col) {}
}
scrollToLine (num) { scrollToLine (num) {}
}
focus () { focus () {
this.editor.focus() this.editor.focus()
@@ -210,64 +401,189 @@ export default class CodeEditor extends React.Component {
this.editor.setCursor(cursor) this.editor.setCursor(cursor)
} }
handleDropImage (e) { handleDropImage (dropEvent) {
e.preventDefault() dropEvent.preventDefault()
const imagePath = e.dataTransfer.files[0].path const { storageKey, noteKey } = this.props
const filename = path.basename(imagePath) attachmentManagement.handleAttachmentDrop(
this,
copyImage(imagePath, this.props.storageKey).then((imagePath) => { storageKey,
const imageMd = `![${filename}](${path.join('/:storage', imagePath)})` noteKey,
this.insertImageMd(imageMd) dropEvent
}) )
} }
insertImageMd (imageMd) { insertAttachmentMd (imageMd) {
this.editor.replaceSelection(imageMd) this.editor.replaceSelection(imageMd)
} }
handlePaste (editor, e) { handlePaste (editor, e) {
const dataTransferItem = e.clipboardData.items[0] const clipboardData = e.clipboardData
if (!dataTransferItem.type.match('image')) return const { storageKey, noteKey } = this.props
const dataTransferItem = clipboardData.items[0]
const blob = dataTransferItem.getAsFile() const pastedTxt = clipboardData.getData('text')
const reader = new FileReader() const isURL = str => {
let base64data const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
return matcher.test(str)
reader.readAsDataURL(blob) }
reader.onloadend = () => { const isInLinkTag = editor => {
base64data = reader.result.replace(/^data:image\/png;base64,/, '') const startCursor = editor.getCursor('start')
base64data += base64data.replace('+', ' ') const prevChar = editor.getRange(
const binaryData = new Buffer(base64data, 'base64').toString('binary') { line: startCursor.line, ch: startCursor.ch - 2 },
const imageName = Math.random().toString(36).slice(-16) { line: startCursor.line, ch: startCursor.ch }
const storagePath = findStorage(this.props.storageKey).path )
const imageDir = path.join(storagePath, 'images') const endCursor = editor.getCursor('end')
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir) const nextChar = editor.getRange(
const imagePath = path.join(imageDir, `${imageName}.png`) { line: endCursor.line, ch: endCursor.ch },
fs.writeFile(imagePath, binaryData, 'binary') { line: endCursor.line, ch: endCursor.ch + 1 }
const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})` )
this.insertImageMd(imageMd) return prevChar === '](' && nextChar === ')'
}
if (dataTransferItem.type.match('image')) {
attachmentManagement.handlePastImageEvent(
this,
storageKey,
noteKey,
dataTransferItem
)
} else if (
this.props.fetchUrlTitle &&
isURL(pastedTxt) &&
!isInLinkTag(editor)
) {
this.handlePasteUrl(e, editor, pastedTxt)
}
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
attachmentManagement
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
.then(modifiedText => {
this.editor.replaceSelection(modifiedText)
})
e.preventDefault()
} }
} }
handleScroll (e) {
if (this.props.onScroll) {
this.props.onScroll(e)
}
}
handlePasteUrl (e, editor, pastedTxt) {
e.preventDefault()
const taggedUrl = `<${pastedTxt}>`
editor.replaceSelection(taggedUrl)
const isImageReponse = response => {
return (
response.headers.has('content-type') &&
response.headers.get('content-type').match(/^image\/.+$/)
)
}
const replaceTaggedUrl = replacement => {
const value = editor.getValue()
const cursor = editor.getCursor()
const newValue = value.replace(taggedUrl, replacement)
const newCursor = Object.assign({}, cursor, {
ch: cursor.ch + newValue.length - value.length
})
editor.setValue(newValue)
editor.setCursor(newCursor)
}
fetch(pastedTxt, {
method: 'get'
})
.then(response => {
if (isImageReponse(response)) {
return this.mapImageResponse(response, pastedTxt)
} else {
return this.mapNormalResponse(response, pastedTxt)
}
})
.then(replacement => {
replaceTaggedUrl(replacement)
})
.catch(e => {
replaceTaggedUrl(pastedTxt)
})
}
mapNormalResponse (response, pastedTxt) {
return this.decodeResponse(response).then(body => {
return new Promise((resolve, reject) => {
try {
const parsedBody = new window.DOMParser().parseFromString(
body,
'text/html'
)
const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
resolve(linkWithTitle)
} catch (e) {
reject(e)
}
})
})
}
mapImageResponse (response, pastedTxt) {
return new Promise((resolve, reject) => {
try {
const url = response.url
const name = url.substring(url.lastIndexOf('/') + 1)
const imageLinkWithName = `![${name}](${pastedTxt})`
resolve(imageLinkWithName)
} catch (e) {
reject(e)
}
})
}
decodeResponse (response) {
const headers = response.headers
const _charset = headers.has('content-type')
? this.extractContentTypeCharset(headers.get('content-type'))
: undefined
return response.arrayBuffer().then(buff => {
return new Promise((resolve, reject) => {
try {
const charset = _charset !== undefined &&
iconv.encodingExists(_charset)
? _charset
: 'utf-8'
resolve(iconv.decode(new Buffer(buff), charset).toString())
} catch (e) {
reject(e)
}
})
})
}
extractContentTypeCharset (contentType) {
return contentType
.split(';')
.filter(str => {
return str.trim().toLowerCase().startsWith('charset')
})
.map(str => {
return str.replace(/['"]/g, '').split('=')[1]
})[0]
}
render () { render () {
const { className, fontSize } = this.props const {className, fontSize} = this.props
let fontFamily = this.props.fontFamily const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
fontFamily = _.isString(fontFamily) && fontFamily.length > 0 const width = this.props.width
? [fontFamily].concat(defaultEditorFontFamily)
: defaultEditorFontFamily
return ( return (
<div <div
className={className == null className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
? 'CodeEditor'
: `CodeEditor ${className}`
}
ref='root' ref='root'
tabIndex='-1' tabIndex='-1'
style={{ style={{
fontFamily: fontFamily.join(', '), fontFamily,
fontSize: fontSize fontSize: fontSize,
width: width
}} }}
onDrop={(e) => this.handleDropImage(e)} onDrop={e => this.handleDropImage(e)}
/> />
) )
} }
@@ -275,6 +591,8 @@ export default class CodeEditor extends React.Component {
CodeEditor.propTypes = { CodeEditor.propTypes = {
value: PropTypes.string, value: PropTypes.string,
enableRulers: PropTypes.bool,
rulers: PropTypes.arrayOf(Number),
mode: PropTypes.string, mode: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
onBlur: PropTypes.func, onBlur: PropTypes.func,

View File

@@ -92,7 +92,9 @@ class MarkdownEditor extends React.Component {
if (this.state.isLocked) return if (this.state.isLocked) return
this.setState({ keyPressed: new Set() }) this.setState({ keyPressed: new Set() })
const { config } = this.props const { config } = this.props
if (config.editor.switchPreview === 'BLUR') { if (config.editor.switchPreview === 'BLUR' ||
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
) {
const cursorPosition = this.refs.code.editor.getCursor() const cursorPosition = this.refs.code.editor.getCursor()
this.setState({ this.setState({
status: 'PREVIEW' status: 'PREVIEW'
@@ -104,6 +106,20 @@ class MarkdownEditor extends React.Component {
} }
} }
handleDoubleClick (e) {
if (this.state.isLocked) return
this.setState({keyPressed: new Set()})
const { config } = this.props
if (config.editor.switchPreview === 'DBL_CLICK') {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
})
}
}
handlePreviewMouseDown (e) { handlePreviewMouseDown (e) {
this.previewMouseDownedAt = new Date() this.previewMouseDownedAt = new Date()
} }
@@ -207,7 +223,7 @@ class MarkdownEditor extends React.Component {
} }
render () { render () {
const { className, value, config, storageKey } = this.props const {className, value, config, storageKey, noteKey} = 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
@@ -242,8 +258,13 @@ class MarkdownEditor extends React.Component {
fontSize={editorFontSize} fontSize={editorFontSize}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorIndentSize} indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey} storageKey={storageKey}
noteKey={noteKey}
fetchUrlTitle={config.editor.fetchUrlTitle}
onChange={(e) => this.handleChange(e)} onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)} onBlur={(e) => this.handleBlur(e)}
/> />
@@ -260,9 +281,14 @@ class MarkdownEditor extends React.Component {
codeBlockFontFamily={config.editor.fontFamily} codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize} indentSize={editorIndentSize}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes}
smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks}
sanitize={config.preview.sanitize}
ref='preview' ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)} onContextMenu={(e) => this.handleContextMenu(e)}
onDoubleClick={(e) => this.handleDoubleClick(e)}
tabIndex='0' tabIndex='0'
value={this.state.renderValue} value={this.state.renderValue}
onMouseUp={(e) => this.handlePreviewMouseUp(e)} onMouseUp={(e) => this.handlePreviewMouseUp(e)}
@@ -270,6 +296,9 @@ class MarkdownEditor extends React.Component {
onCheckboxClick={(e) => this.handleCheckboxClick(e)} onCheckboxClick={(e) => this.handleCheckboxClick(e)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
/> />
</div> </div>
) )

745
browser/components/MarkdownPreview.js Normal file → Executable file
View File

@@ -1,30 +1,51 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import markdown from 'browser/lib/markdown' import Markdown from 'browser/lib/markdown'
import _ from 'lodash' import _ from 'lodash'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir' import 'codemirror-mode-elixir'
import consts from 'browser/lib/consts' import consts from 'browser/lib/consts'
import Raphael from 'raphael' import Raphael from 'raphael'
import flowchart from 'flowchart' import flowchart from 'flowchart'
import mermaidRender from './render/MermaidRender'
import SequenceDiagram from 'js-sequence-diagrams' import SequenceDiagram from 'js-sequence-diagrams'
import Chart from 'chart.js'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import fs from 'fs'
import htmlTextHelper from 'browser/lib/htmlTextHelper' import htmlTextHelper from 'browser/lib/htmlTextHelper'
import convertModeName from 'browser/lib/convertModeName'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import mdurl from 'mdurl' import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote'
import { escapeHtmlCharacters } from 'browser/lib/utils'
const { remote } = require('electron') const { remote } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const { app } = remote const { app } = remote
const path = require('path') const path = require('path')
const fileUrl = require('file-url')
const dialog = remote.dialog const dialog = remote.dialog
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = 'file://' + (process.env.NODE_ENV === 'production' const appPath = fileUrl(
? app.getAppPath() process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
: path.resolve()) )
const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`
]
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) { function buildStyle (
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
) {
return ` return `
@font-face { @font-face {
font-family: 'Lato'; font-family: 'Lato';
@@ -44,10 +65,23 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
font-weight: 700; font-weight: 700;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${allowCustomCSS ? customCSS : ''}
${markdownStyle} ${markdownStyle}
body { body {
font-family: '${fontFamily.join("','")}'; font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px; font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'}
} }
code { code {
font-family: '${codeBlockFontFamily.join("','")}'; font-family: '${codeBlockFontFamily.join("','")}';
@@ -95,9 +129,38 @@ h2 {
body p { body p {
white-space: normal; white-space: normal;
} }
@media print {
body[data-theme="${theme}"] {
color: #000;
background-color: #fff;
}
.clipboardButton {
display: none
}
}
` `
} }
const scrollBarStyle = `
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
}
`
const scrollBarDarkStyle = `
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3);
}
`
const { shell } = require('electron') const { shell } = require('electron')
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -106,52 +169,65 @@ if (!OSX) {
defaultFontFamily.unshift('Microsoft YaHei') defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo') defaultFontFamily.unshift('meiryo')
} }
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] const defaultCodeBlockFontFamily = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
export default class MarkdownPreview extends React.Component { export default class MarkdownPreview extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.contextMenuHandler = (e) => this.handleContextMenu(e) this.contextMenuHandler = e => this.handleContextMenu(e)
this.mouseDownHandler = (e) => this.handleMouseDown(e) this.mouseDownHandler = e => this.handleMouseDown(e)
this.mouseUpHandler = (e) => this.handleMouseUp(e) this.mouseUpHandler = e => this.handleMouseUp(e)
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e) this.DoubleClickHandler = e => this.handleDoubleClick(e)
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
leading: false,
trailing: true
})
this.checkboxClickHandler = e => this.handleCheckboxClick(e)
this.saveAsTextHandler = () => this.handleSaveAsText() this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd() this.saveAsMdHandler = () => this.handleSaveAsMd()
this.saveAsHtmlHandler = () => this.handleSaveAsHtml() this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
this.printHandler = () => this.handlePrint() this.printHandler = () => this.handlePrint()
this.linkClickHandler = this.handlelinkClick.bind(this) this.linkClickHandler = this.handlelinkClick.bind(this)
this.initMarkdown = this.initMarkdown.bind(this)
this.initMarkdown()
} }
handlePreviewAnchorClick (e) { initMarkdown () {
e.preventDefault() const { smartQuotes, sanitize, breaks } = this.props
e.stopPropagation() this.markdown = new Markdown({
typographer: smartQuotes,
const anchor = e.target.closest('a') sanitize,
const href = anchor.getAttribute('href') breaks
if (_.isString(href) && href.match(/^#/)) { })
const targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
if (targetElement != null) {
this.getWindow().scrollTo(0, targetElement.offsetTop)
}
} else {
shell.openExternal(href)
}
} }
handleCheckboxClick (e) { handleCheckboxClick (e) {
this.props.onCheckboxClick(e) this.props.onCheckboxClick(e)
} }
handleScroll (e) {
if (this.props.onScroll) {
this.props.onScroll(e)
}
}
handleContextMenu (e) { handleContextMenu (e) {
if (!this.props.onContextMenu) return
this.props.onContextMenu(e) this.props.onContextMenu(e)
} }
handleDoubleClick (e) {
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
}
handleMouseDown (e) { handleMouseDown (e) {
if (!this.props.onMouseDown) return
if (e.target != null) { if (e.target != null) {
switch (e.target.tagName) { switch (e.target.tagName) {
case 'A': case 'A':
@@ -175,12 +251,97 @@ export default class MarkdownPreview extends React.Component {
} }
handleSaveAsMd () { handleSaveAsMd () {
this.exportAsDocument('md') this.exportAsDocument('md', (noteContent, exportTasks) => {
let result = noteContent
if (this.props && this.props.storagePath && this.props.noteKey) {
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
result = attachmentManagement.removeStorageAndNoteReferences(
noteContent,
this.props.noteKey
)
}
return result
})
} }
handleSaveAsHtml () { handleSaveAsHtml () {
this.exportAsDocument('html', (value) => { this.exportAsDocument('html', (noteContent, exportTasks) => {
return this.refs.root.contentWindow.document.documentElement.outerHTML const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
const inlineStyles = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
let body = this.markdown.render(
escapeHtmlCharacters(noteContent, { detectCodeBlock: true })
)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
files.forEach(file => {
if (global.process.platform === 'win32') {
file = file.replace('file:///', '')
} else {
file = file.replace('file://', '')
}
exportTasks.push({
src: file,
dst: 'css'
})
})
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
body = attachmentManagement.removeStorageAndNoteReferences(
body,
this.props.noteKey
)
let styles = ''
files.forEach(file => {
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
})
return `<html>
<head>
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
</head>
<body>${body}</body>
</html>`
}) })
} }
@@ -188,53 +349,108 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.print() this.refs.root.contentWindow.print()
} }
exportAsDocument (fileType, formatter) { exportAsDocument (fileType, contentFormatter) {
const options = { const options = {
filters: [ filters: [{ name: 'Documents', extensions: [fileType] }],
{ name: 'Documents', extensions: [fileType] }
],
properties: ['openFile', 'createDirectory'] properties: ['openFile', 'createDirectory']
} }
const value = formatter ? formatter.call(this, this.props.value) : this.props.value
dialog.showSaveDialog(remote.getCurrentWindow(), options, dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
(filename) => {
if (filename) { if (filename) {
fs.writeFile(filename, value, (err) => { const content = this.props.value
if (err) throw err const storage = this.props.storagePath
})
exportNote(storage, content, filename, contentFormatter)
.then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${filename}`
})
})
.catch(err => {
dialog.showErrorBox(
'Export error',
err ? err.message || err : 'Unexpected error during export'
)
throw err
})
} }
}) })
} }
fixDecodedURI (node) { fixDecodedURI (node) {
if (node && node.children.length === 1 && typeof node.children[0] === 'string') { if (
node &&
node.children.length === 1 &&
typeof node.children[0] === 'string'
) {
const { innerText, href } = node const { innerText, href } = node
node.innerText = mdurl.decode(href) === innerText node.innerText = mdurl.decode(href) === innerText ? href : innerText
? href }
: innerText }
getScrollBarStyle () {
const { theme } = this.props
switch (theme) {
case 'dark':
case 'solarized-dark':
case 'monokai':
return scrollBarDarkStyle
default:
return scrollBarStyle
} }
} }
componentDidMount () { componentDidMount () {
this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler) this.refs.root.contentWindow.document.body.addEventListener(
'contextmenu',
this.contextMenuHandler
)
this.refs.root.contentWindow.document.head.innerHTML = ` let styles = `
<style id='style'></style> <style id='style'></style>
<link rel="stylesheet" href="${appPath}/node_modules/katex/dist/katex.min.css">
<link rel="stylesheet" href="${appPath}/node_modules/codemirror/lib/codemirror.css">
<link rel="stylesheet" id="codeTheme"> <link rel="stylesheet" id="codeTheme">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
${this.getScrollBarStyle()}
</style>
` `
CSS_FILES.forEach(file => {
styles += `<link rel="stylesheet" href="${file}">`
})
this.refs.root.contentWindow.document.head.innerHTML = styles
this.rewriteIframe() this.rewriteIframe()
this.applyStyle() this.applyStyle()
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler) this.refs.root.contentWindow.document.addEventListener(
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler) 'mousedown',
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler) this.mouseDownHandler
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler) )
this.refs.root.contentWindow.document.addEventListener(
'mouseup',
this.mouseUpHandler
)
this.refs.root.contentWindow.document.addEventListener(
'dblclick',
this.DoubleClickHandler
)
this.refs.root.contentWindow.document.addEventListener(
'drop',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.addEventListener(
'dragover',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.addEventListener(
'scroll',
this.scrollHandler
)
eventEmitter.on('export:save-text', this.saveAsTextHandler) eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
@@ -242,11 +458,34 @@ export default class MarkdownPreview extends React.Component {
} }
componentWillUnmount () { componentWillUnmount () {
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler) this.refs.root.contentWindow.document.body.removeEventListener(
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler) 'contextmenu',
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler) this.contextMenuHandler
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler) )
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.removeEventListener(
'mousedown',
this.mouseDownHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'mouseup',
this.mouseUpHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'dblclick',
this.DoubleClickHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'drop',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'dragover',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'scroll',
this.scrollHandler
)
eventEmitter.off('export:save-text', this.saveAsTextHandler) eventEmitter.off('export:save-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('export:save-md', this.saveAsMdHandler)
eventEmitter.off('export:save-html', this.saveAsHtmlHandler) eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
@@ -255,122 +494,195 @@ export default class MarkdownPreview extends React.Component {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe() if (prevProps.value !== this.props.value) this.rewriteIframe()
if (prevProps.fontFamily !== this.props.fontFamily || if (
prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.sanitize !== this.props.sanitize ||
prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks
) {
this.initMarkdown()
this.rewriteIframe()
}
if (
prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize || prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme || prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
prevProps.lineNumber !== this.props.lineNumber || prevProps.lineNumber !== this.props.lineNumber ||
prevProps.showCopyNotification !== this.props.showCopyNotification || prevProps.showCopyNotification !== this.props.showCopyNotification ||
prevProps.theme !== this.props.theme) { prevProps.theme !== this.props.theme ||
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
prevProps.customCSS !== this.props.customCSS
) {
this.applyStyle() this.applyStyle()
this.rewriteIframe() this.rewriteIframe()
} }
} }
applyStyle () { getStyleParams () {
const { fontSize, lineNumber, codeBlockTheme } = this.props const {
fontSize,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.props
let { fontFamily, codeBlockFontFamily } = this.props let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily) ? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily : defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0 codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily : defaultCodeBlockFontFamily
this.setCodeTheme(codeBlockTheme) return {
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
}
} }
setCodeTheme (theme) { applyStyle () {
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default' const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
this.getWindow().document.getElementById(
'codeTheme'
).href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
}
GetCodeThemeLink (theme) {
theme = consts.THEMES.some(_theme => _theme === theme) &&
theme !== 'default'
? theme ? theme
: 'elegant' : 'elegant'
this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized') return theme.startsWith('solarized')
? `${appPath}/node_modules/codemirror/theme/solarized.css` ? `${appPath}/node_modules/codemirror/theme/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css` : `${appPath}/node_modules/codemirror/theme/${theme}.css`
} }
rewriteIframe () { rewriteIframe () {
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { _.forEach(
el.removeEventListener('click', this.anchorClickHandler) this.refs.root.contentWindow.document.querySelectorAll(
}) 'input[type="checkbox"]'
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { ),
el.removeEventListener('click', this.checkboxClickHandler) el => {
}) el.removeEventListener('click', this.checkboxClickHandler)
}
)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { _.forEach(
el.removeEventListener('click', this.linkClickHandler) this.refs.root.contentWindow.document.querySelectorAll('a'),
}) el => {
el.removeEventListener('click', this.linkClickHandler)
}
)
const { theme, indentSize, showCopyNotification, storagePath } = this.props const {
theme,
indentSize,
showCopyNotification,
storagePath,
noteKey
} = this.props
let { value, codeBlockTheme } = this.props let { value, codeBlockTheme } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
const renderedHTML = this.markdown.render(value)
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
renderedHTML,
storagePath
)
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll(
'input[type="checkbox"]'
),
el => {
el.addEventListener('click', this.checkboxClickHandler)
}
)
const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g) _.forEach(
if (codeBlocks !== null) { this.refs.root.contentWindow.document.querySelectorAll('a'),
codeBlocks.forEach((codeBlock) => { el => {
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) this.fixDecodedURI(el)
}) el.addEventListener('click', this.linkClickHandler)
} }
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value) )
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => { codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
el.parentNode.parentNode.style.listStyleType = 'none'
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el)
el.addEventListener('click', this.anchorClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
el.addEventListener('click', this.checkboxClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.addEventListener('click', this.linkClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
el.src = markdown.normalizeLinkText(el.src)
if (!/\/:storage/.test(el.src)) return
el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
})
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
? codeBlockTheme ? codeBlockTheme
: 'default' : 'default'
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => { _.forEach(
let syntax = CodeMirror.findModeByName(el.className) this.refs.root.contentWindow.document.querySelectorAll('.code code'),
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') el => {
CodeMirror.requireMode(syntax.mode, () => { let syntax = CodeMirror.findModeByName(convertModeName(el.className))
const content = htmlTextHelper.decodeEntities(el.innerHTML) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
const copyIcon = document.createElement('i') CodeMirror.requireMode(syntax.mode, () => {
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>' const content = htmlTextHelper.decodeEntities(el.innerHTML)
copyIcon.onclick = (e) => { const copyIcon = document.createElement('i')
copy(content) copyIcon.innerHTML =
if (showCopyNotification) { '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
this.notify('Saved to Clipboard!', { copyIcon.onclick = e => {
body: 'Paste it wherever you want!', copy(content)
silent: true if (showCopyNotification) {
}) this.notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
}
} }
} el.parentNode.appendChild(copyIcon)
el.parentNode.appendChild(copyIcon) el.innerHTML = ''
el.innerHTML = '' if (codeBlockTheme.indexOf('solarized') === 0) {
if (codeBlockTheme.indexOf('solarized') === 0) { const [refThema, color] = codeBlockTheme.split(' ')
const [refThema, color] = codeBlockTheme.split(' ') el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror` } else {
} else { el.parentNode.className += ` cm-s-${codeBlockTheme}`
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror` }
} CodeMirror.runMode(content, syntax.mime, el, {
CodeMirror.runMode(content, syntax.mime, el, { tabSize: indentSize
tabSize: indentSize })
}) })
}) }
}) )
const opts = {} const opts = {}
// if (this.props.theme === 'dark') { // if (this.props.theme === 'dark') {
// opts['font-color'] = '#DDD' // opts['font-color'] = '#DDD'
@@ -378,37 +690,71 @@ export default class MarkdownPreview extends React.Component {
// opts['element-color'] = '#DDD' // opts['element-color'] = '#DDD'
// opts['fill'] = '#3A404C' // opts['fill'] = '#3A404C'
// } // }
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => { _.forEach(
Raphael.setWindow(this.getWindow()) this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
try { el => {
const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML)) Raphael.setWindow(this.getWindow())
el.innerHTML = '' try {
diagram.drawSVG(el, opts) const diagram = flowchart.parse(
_.forEach(el.querySelectorAll('a'), (el) => { htmlTextHelper.decodeEntities(el.innerHTML)
el.addEventListener('click', this.anchorClickHandler) )
}) el.innerHTML = ''
} catch (e) { diagram.drawSVG(el, opts)
console.error(e) _.forEach(el.querySelectorAll('a'), el => {
el.className = 'flowchart-error' el.addEventListener('click', this.linkClickHandler)
el.innerHTML = 'Flowchart parse error: ' + e.message })
} catch (e) {
console.error(e)
el.className = 'flowchart-error'
el.innerHTML = 'Flowchart parse error: ' + e.message
}
} }
}) )
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => { _.forEach(
Raphael.setWindow(this.getWindow()) this.refs.root.contentWindow.document.querySelectorAll('.sequence'),
try { el => {
const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML)) Raphael.setWindow(this.getWindow())
el.innerHTML = '' try {
diagram.drawSVG(el, {theme: 'simple'}) const diagram = SequenceDiagram.parse(
_.forEach(el.querySelectorAll('a'), (el) => { htmlTextHelper.decodeEntities(el.innerHTML)
el.addEventListener('click', this.anchorClickHandler) )
}) el.innerHTML = ''
} catch (e) { diagram.drawSVG(el, { theme: 'simple' })
console.error(e) _.forEach(el.querySelectorAll('a'), el => {
el.className = 'sequence-error' el.addEventListener('click', this.linkClickHandler)
el.innerHTML = 'Sequence diagram parse error: ' + e.message })
} catch (e) {
console.error(e)
el.className = 'sequence-error'
el.innerHTML = 'Sequence diagram parse error: ' + e.message
}
} }
}) )
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
el => {
try {
const chartConfig = JSON.parse(el.innerHTML)
el.innerHTML = ''
var canvas = document.createElement('canvas')
el.appendChild(canvas)
/* eslint-disable no-new */
new Chart(canvas, chartConfig)
} catch (e) {
console.error(e)
el.className = 'chart-error'
el.innerHTML = 'chartjs diagram parse error: ' + e.message
}
}
)
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
el => {
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
}
)
} }
focus () { focus () {
@@ -420,7 +766,9 @@ export default class MarkdownPreview extends React.Component {
} }
scrollTo (targetRow) { scrollTo (targetRow) {
const blocks = this.getWindow().document.querySelectorAll('body>[data-line]') const blocks = this.getWindow().document.querySelectorAll(
'body>[data-line]'
)
for (let index = 0; index < blocks.length; index++) { for (let index = 0; index < blocks.length; index++) {
let block = blocks[index] let block = blocks[index]
@@ -440,25 +788,64 @@ export default class MarkdownPreview extends React.Component {
notify (title, options) { notify (title, options) {
if (global.process.platform === 'win32') { if (global.process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png') options.icon = path.join(
'file://',
global.__dirname,
'../../resources/app.png'
)
} }
return new window.Notification(title, options) return new window.Notification(title, options)
} }
handlelinkClick (e) { handlelinkClick (e) {
const noteHash = e.target.href.split('/').pop() e.preventDefault()
const regexIsNoteLink = /^(.{20})-(.{20})$/ e.stopPropagation()
if (regexIsNoteLink.test(noteHash)) {
eventEmitter.emit('list:jump', noteHash) const href = e.target.href
const linkHash = href.split('/').pop()
const regexNoteInternalLink = /main.html#(.+)/
if (regexNoteInternalLink.test(linkHash)) {
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
const targetElement = this.refs.root.contentWindow.document.getElementById(
targetId
)
if (targetElement != null) {
this.getWindow().scrollTo(0, targetElement.offsetTop)
}
return
} }
// this will match the new uuid v4 hash and the old hash
// e.g.
// :note:1c211eb7dcb463de6490 and
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
if (regexIsNoteLink.test(linkHash)) {
eventEmitter.emit('list:jump', linkHash.replace(':note:', ''))
return
}
// this will match the old link format storage.key-note.key
// e.g.
// 877f99c3268608328037-1c211eb7dcb463de6490
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
if (regexIsLegacyNoteLink.test(linkHash)) {
eventEmitter.emit('list:jump', linkHash.split('-')[1])
return
}
// other case
shell.openExternal(href)
} }
render () { render () {
const { className, style, tabIndex } = this.props const { className, style, tabIndex } = this.props
return ( return (
<iframe className={className != null <iframe
? 'MarkdownPreview ' + className className={
: 'MarkdownPreview' className != null ? 'MarkdownPreview ' + className : 'MarkdownPreview'
} }
style={style} style={style}
tabIndex={tabIndex} tabIndex={tabIndex}
@@ -473,8 +860,12 @@ MarkdownPreview.propTypes = {
onDoubleClick: PropTypes.func, onDoubleClick: PropTypes.func,
onMouseUp: PropTypes.func, onMouseUp: PropTypes.func,
onMouseDown: PropTypes.func, onMouseDown: PropTypes.func,
onContextMenu: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
showCopyNotification: PropTypes.bool, showCopyNotification: PropTypes.bool,
storagePath: PropTypes.string storagePath: PropTypes.string,
smartQuotes: PropTypes.bool,
smartArrows: PropTypes.bool,
breaks: PropTypes.bool
} }

View File

@@ -2,6 +2,7 @@ import React from 'react'
import CodeEditor from 'browser/components/CodeEditor' import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview' import MarkdownPreview from 'browser/components/MarkdownPreview'
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
import _ from 'lodash'
import styles from './MarkdownSplitEditor.styl' import styles from './MarkdownSplitEditor.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -12,6 +13,11 @@ class MarkdownSplitEditor extends React.Component {
this.value = props.value this.value = props.value
this.focus = () => this.refs.code.focus() this.focus = () => this.refs.code.focus()
this.reload = () => this.refs.code.reload() this.reload = () => this.refs.code.reload()
this.userScroll = true
this.state = {
isSliderFocused: false,
codeEditorWidthInPercent: 50
}
} }
handleOnChange () { handleOnChange () {
@@ -19,6 +25,49 @@ class MarkdownSplitEditor extends React.Component {
this.props.onChange() this.props.onChange()
} }
handleScroll (e) {
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
const codeDoc = _.get(this, 'refs.code.editor.doc')
let srcTop, srcHeight, targetTop, targetHeight
if (this.userScroll) {
if (e.doc) {
srcTop = _.get(e, 'doc.scrollTop')
srcHeight = _.get(e, 'doc.height')
targetTop = _.get(previewDoc, 'body.scrollTop')
targetHeight = _.get(previewDoc, 'body.scrollHeight')
} else {
srcTop = _.get(previewDoc, 'body.scrollTop')
srcHeight = _.get(previewDoc, 'body.scrollHeight')
targetTop = _.get(codeDoc, 'scrollTop')
targetHeight = _.get(codeDoc, 'height')
}
const distance = (targetHeight * srcTop / srcHeight) - targetTop
const framerate = 1000 / 60
const frames = 20
const refractory = frames * framerate
this.userScroll = false
let frame = 0
let scrollPos, time
const timer = setInterval(() => {
time = frame / frames
scrollPos = time < 0.5
? 2 * time * time // ease in
: -1 + (4 - 2 * time) * time // ease out
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance)
if (frame >= frames) {
clearInterval(timer)
setTimeout(() => { this.userScroll = true }, refractory)
}
frame++
}, framerate)
}
}
handleCheckboxClick (e) { handleCheckboxClick (e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@@ -42,32 +91,81 @@ class MarkdownSplitEditor extends React.Component {
} }
} }
handleMouseMove (e) {
if (this.state.isSliderFocused) {
const rootRect = this.refs.root.getBoundingClientRect()
const rootWidth = rootRect.width
const offset = rootRect.left
let newCodeEditorWidthInPercent = (e.pageX - offset) / rootWidth * 100
// limit minSize to 10%, maxSize to 90%
if (newCodeEditorWidthInPercent <= 10) {
newCodeEditorWidthInPercent = 10
}
if (newCodeEditorWidthInPercent >= 90) {
newCodeEditorWidthInPercent = 90
}
this.setState({
codeEditorWidthInPercent: newCodeEditorWidthInPercent
})
}
}
handleMouseUp (e) {
e.preventDefault()
this.setState({
isSliderFocused: false
})
}
handleMouseDown (e) {
e.preventDefault()
this.setState({
isSliderFocused: true
})
}
render () { render () {
const { config, value, storageKey } = this.props const {config, value, storageKey, noteKey} = this.props
const storage = findStorage(storageKey) const storage = findStorage(storageKey)
let editorFontSize = parseInt(config.editor.fontSize, 10) let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10) let editorIndentSize = parseInt(config.editor.indentSize, 10)
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' previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%'
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
return ( return (
<div styleName='root'> <div styleName='root' ref='root'
onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}>
<CodeEditor <CodeEditor
styleName='codeEditor' styleName='codeEditor'
ref='code' ref='code'
width={this.state.codeEditorWidthInPercent + '%'}
mode='GitHub Flavored Markdown' mode='GitHub Flavored Markdown'
value={value} value={value}
theme={config.editor.theme} theme={config.editor.theme}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily} fontFamily={config.editor.fontFamily}
fontSize={editorFontSize} fontSize={editorFontSize}
displayLineNumbers={config.editor.displayLineNumbers}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorIndentSize} indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
storageKey={storageKey} storageKey={storageKey}
noteKey={noteKey}
onChange={this.handleOnChange.bind(this)} onChange={this.handleOnChange.bind(this)}
onScroll={this.handleScroll.bind(this)}
/> />
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
<div styleName='slider-hitbox' />
</div>
<MarkdownPreview <MarkdownPreview
style={previewStyle} style={previewStyle}
styleName='preview' styleName='preview'
@@ -78,12 +176,21 @@ class MarkdownSplitEditor extends React.Component {
codeBlockTheme={config.preview.codeBlockTheme} codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily} codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes}
smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks}
sanitize={config.preview.sanitize}
ref='preview' ref='preview'
tabInde='0' tabInde='0'
value={value} value={value}
onCheckboxClick={(e) => this.handleCheckboxClick(e)} onCheckboxClick={(e) => this.handleCheckboxClick(e)}
onScroll={this.handleScroll.bind(this)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
/> />
</div> </div>
) )

View File

@@ -3,7 +3,14 @@
height 100% height 100%
font-size 30px font-size 30px
display flex display flex
.codeEditor .slider
width 50% absolute top bottom
.preview top -2px
width 50% width 0
z-index 0
.slider-hitbox
absolute top bottom left right
width 7px
left -3px
z-index 10
cursor col-resize

View File

@@ -8,6 +8,7 @@ import CSSModules from 'browser/lib/CSSModules'
import { getTodoStatus } from 'browser/lib/getTodoStatus' import { getTodoStatus } from 'browser/lib/getTodoStatus'
import styles from './NoteItem.styl' import styles from './NoteItem.styl'
import TodoProcess from './TodoProcess' import TodoProcess from './TodoProcess'
import i18n from 'browser/lib/i18n'
/** /**
* @description Tag element component. * @description Tag element component.
@@ -25,14 +26,12 @@ const TagElement = ({ tagName }) => (
* @param {Array|null} tags * @param {Array|null} tags
* @return {React.Component} * @return {React.Component}
*/ */
const TagElementList = (tags) => { const TagElementList = tags => {
if (!isArray(tags)) { if (!isArray(tags)) {
return [] return []
} }
const tagElements = tags.map(tag => ( const tagElements = tags.map(tag => TagElement({ tagName: tag }))
TagElement({tagName: tag})
))
return tagElements return tagElements
} }
@@ -46,46 +45,77 @@ const TagElementList = (tags) => {
* @param {Function} handleDragStart * @param {Function} handleDragStart
* @param {string} dateDisplay * @param {string} dateDisplay
*/ */
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => ( const NoteItem = ({
<div styleName={isActive isActive,
? 'item--active' note,
: 'item' dateDisplay,
} handleNoteClick,
key={`${note.storage}-${note.key}`} handleNoteContextMenu,
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)} handleDragStart,
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)} pathname,
storageName,
folderName,
viewType
}) => (
<div
styleName={isActive ? 'item--active' : 'item'}
key={note.key}
onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)}
onDragStart={e => handleDragStart(e, note)} onDragStart={e => handleDragStart(e, note)}
draggable='true' draggable='true'
> >
<div styleName='item-wrapper'> <div styleName='item-wrapper'>
{note.type === 'SNIPPET_NOTE' {note.type === 'SNIPPET_NOTE'
? <i styleName='item-title-icon' className='fa fa-fw fa-code' /> ? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' /> : <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
}
<div styleName='item-title'> <div styleName='item-title'>
{note.title.trim().length > 0 {note.title.trim().length > 0
? note.title ? note.title
: <span styleName='item-title-empty'>Empty</span> : <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
}
</div> </div>
{['ALL', 'STORAGE'].includes(viewType) &&
<div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div>
<div styleName='item-middle-app-meta'>
<div
title={
viewType === 'ALL'
? storageName
: viewType === 'STORAGE' ? folderName : null
}
styleName='item-middle-app-meta-label'
>
{viewType === 'ALL' && storageName}
{viewType === 'STORAGE' && folderName}
</div>
</div>
</div>}
<div styleName='item-bottom-time'>{dateDisplay}</div>
{note.isStarred
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
}
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
}
{note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
: ''
}
<div styleName='item-bottom'> <div styleName='item-bottom'>
<div styleName='item-bottom-tagList'> <div styleName='item-bottom-tagList'>
{note.tags.length > 0 {note.tags.length > 0
? TagElementList(note.tags) ? TagElementList(note.tags)
: <span styleName='item-bottom-tagList-empty' /> : <span
} style={{ fontStyle: 'italic', opacity: 0.5 }}
styleName='item-bottom-tagList-empty'
>
{i18n.__('No tags')}
</span>}
</div>
<div>
{note.isStarred
? <img
styleName='item-star'
src='../resources/icon/icon-starred.svg'
/>
: ''}
{note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' />
: ''}
{note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
: ''}
</div> </div>
</div> </div>
</div> </div>
@@ -102,7 +132,11 @@ NoteItem.propTypes = {
title: PropTypes.string.isrequired, title: PropTypes.string.isrequired,
tags: PropTypes.array, tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired, isStarred: PropTypes.bool.isRequired,
isTrashed: PropTypes.bool.isRequired isTrashed: PropTypes.bool.isRequired,
blog: {
blogLink: PropTypes.string,
blogId: PropTypes.number
}
}), }),
handleNoteClick: PropTypes.func.isRequired, handleNoteClick: PropTypes.func.isRequired,
handleNoteContextMenu: PropTypes.func.isRequired, handleNoteContextMenu: PropTypes.func.isRequired,

View File

@@ -90,6 +90,26 @@ $control-height = 30px
font-weight normal font-weight normal
color $ui-inactive-text-color color $ui-inactive-text-color
.item-middle
font-size 13px
padding-left 2px
padding-bottom 2px
.item-middle-time
color $ui-inactive-text-color
display inline-block
.item-middle-app-meta
float right
.item-middle-app-meta-label
opacity 0.4
color $ui-inactive-text-color
padding 0 3px
white-space nowrap
text-overflow ellipsis
overflow hidden
max-width 200px
.item-bottom .item-bottom
position relative position relative
bottom 0px bottom 0px
@@ -97,7 +117,7 @@ $control-height = 30px
font-size 12px font-size 12px
line-height 20px line-height 20px
overflow ellipsis overflow ellipsis
display flex display block
.item-bottom-tagList .item-bottom-tagList
flex 1 flex 1
@@ -125,10 +145,8 @@ $control-height = 30px
.item-star .item-star
position absolute position absolute
right -6px right 2px
bottom 23px top 5px
width 16px
height 16px
color alpha($ui-favorite-star-button-color, 60%) color alpha($ui-favorite-star-button-color, 60%)
font-size 12px font-size 12px
padding 0 padding 0
@@ -136,10 +154,8 @@ $control-height = 30px
.item-pin .item-pin
position absolute position absolute
right 0px right 25px
bottom 2px top 7px
width 34px
height 34px
color #E54D42 color #E54D42
font-size 14px font-size 14px
padding 0 padding 0
@@ -305,3 +321,76 @@ body[data-theme="solarized-dark"]
.item-bottom-tagList-empty .item-bottom-tagList-empty
color $ui-inactive-text-color color $ui-inactive-text-color
vertical-align middle vertical-align middle
body[data-theme="monokai"]
.root
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteList-backgroundColor
.item
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteList-backgroundColor
&: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
color $ui-monokai-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-monokai-noteList-backgroundColor, 20%)
color $ui-monokai-text-color
&:active
transition 0.15s
background-color $ui-monokai-noteList-backgroundColor
color $ui-monokai-text-color
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-monokai-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
border-color transparent
.item-title
.item-title-icon
.item-bottom-time
color $ui-monokai-text-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 #c0392b
.item-bottom-tagList-item
background-color alpha(#fff, 20%)
.item-title
color $ui-inactive-text-color
.item-title-icon
color $ui-inactive-text-color
.item-title-empty
color $ui-inactive-text-color
.item-bottom-tagList-item
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
color $ui-inactive-text-color
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle

View File

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

View File

@@ -104,6 +104,7 @@ body[data-theme="dark"]
background-color alpha($ui-dark-button--active-backgroundColor, 20%) background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-dark-text-color color $ui-dark-text-color
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
@@ -117,6 +118,7 @@ body[data-theme="dark"]
background-color $ui-dark-button--active-backgroundColor background-color $ui-dark-button--active-backgroundColor
color $ui-dark-text-color color $ui-dark-text-color
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
@@ -132,6 +134,7 @@ body[data-theme="dark"]
.item-simple-wrapper .item-simple-wrapper
border-color transparent border-color transparent
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
color $ui-dark-text-color color $ui-dark-text-color
@@ -165,9 +168,10 @@ body[data-theme="solarized-dark"]
background-color $ui-solarized-dark-noteList-backgroundColor background-color $ui-solarized-dark-noteList-backgroundColor
&:hover &:hover
transition 0.15s transition 0.15s
// background-color alpha($ui-dark-button--active-backgroundColor, 20%) background-color alpha($ui-dark-button--active-backgroundColor, 60%)
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
@@ -178,9 +182,10 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
&:active &:active
transition 0.15s transition 0.15s
background-color $ui-solarized-dark-button--active-backgroundColor // background-color $ui-solarized-dark-button--active-backgroundColor
color $ui-solarized-dark-text-color color $ui-dark-text-color
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
@@ -192,11 +197,13 @@ body[data-theme="solarized-dark"]
.item-simple--active .item-simple--active
border-color $ui-solarized-dark-borderColor border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-button--active-backgroundColor background-color $ui-solarized-dark-tag-backgroundColor
.item-simple-wrapper .item-simple-wrapper
border-color transparent border-color transparent
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
color $ui-dark-text-color
.item-simple-bottom-time .item-simple-bottom-time
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
@@ -207,3 +214,75 @@ body[data-theme="solarized-dark"]
color #c0392b color #c0392b
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
background-color alpha(#fff, 20%) background-color alpha(#fff, 20%)
.item-simple-title
color $ui-dark-text-color
border-bottom $ui-dark-borderColor
.item-simple-right
float right
.item-simple-right-storageName
padding-left 4px
opacity 0.4
body[data-theme="monokai"]
.root
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteList-backgroundColor
.item-simple
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteList-backgroundColor
&:hover
transition 0.15s
background-color alpha($ui-monokai-button-backgroundColor, 60%)
color $ui-monokai-text-color
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-solarized-dark-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(#fff, 20%)
color $ui-monokai-text-color
&:active
transition 0.15s
background-color $ui-monokai-button--active-backgroundColor
color $ui-monokai-text-color
.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-empty
.item-simple-title-icon
.item-simple-bottom-time
color $ui-monokai-text-color
.item-simple-bottom-tagList-item
background-color alpha(white, 10%)
color $ui-monokai-text-color
&: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

View File

@@ -41,3 +41,14 @@ body[data-theme="solarized-dark"]
background-color $ui-solarized-dark-button-backgroundColor background-color $ui-solarized-dark-button-backgroundColor
&:hover &:hover
color #5CB85C color #5CB85C
body[data-theme="monokai"]
.notification-area
background-color none
.notification-link
color $ui-monokai-text-color
border none
background-color $ui-monokai-button-backgroundColor
&:hover
color #5CB85C

View File

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SideNavFilter.styl' import styles from './SideNavFilter.styl'
import i18n from 'browser/lib/i18n'
/** /**
* @param {boolean} isFolded * @param {boolean} isFolded
@@ -17,7 +18,7 @@ import styles from './SideNavFilter.styl'
const SideNavFilter = ({ const SideNavFilter = ({
isFolded, isHomeActive, handleAllNotesButtonClick, isFolded, isHomeActive, handleAllNotesButtonClick,
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote, isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
counterTotalNote, counterStarredNote counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
}) => ( }) => (
<div styleName={isFolded ? 'menu--folded' : 'menu'}> <div styleName={isFolded ? 'menu--folded' : 'menu'}>
@@ -26,12 +27,12 @@ const SideNavFilter = ({
> >
<div styleName='iconWrap'> <div styleName='iconWrap'>
<img src={isHomeActive <img src={isHomeActive
? '../resources/icon/icon-all-active.svg' ? '../resources/icon/icon-all-active.svg'
: '../resources/icon/icon-all.svg' : '../resources/icon/icon-all.svg'
} }
/> />
</div> </div>
<span styleName='menu-button-label'>All Notes</span> <span styleName='menu-button-label'>{i18n.__('All Notes')}</span>
<span styleName='counters'>{counterTotalNote}</span> <span styleName='counters'>{counterTotalNote}</span>
</button> </button>
@@ -40,26 +41,26 @@ const SideNavFilter = ({
> >
<div styleName='iconWrap'> <div styleName='iconWrap'>
<img src={isStarredActive <img src={isStarredActive
? '../resources/icon/icon-star-active.svg' ? '../resources/icon/icon-star-active.svg'
: '../resources/icon/icon-star-sidenav.svg' : '../resources/icon/icon-star-sidenav.svg'
} }
/> />
</div> </div>
<span styleName='menu-button-label'>Starred</span> <span styleName='menu-button-label'>{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 styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
onClick={handleTrashedButtonClick} onClick={handleTrashedButtonClick} onContextMenu={handleFilterButtonContextMenu}
> >
<div styleName='iconWrap'> <div styleName='iconWrap'>
<img src={isTrashedActive <img src={isTrashedActive
? '../resources/icon/icon-trash-active.svg' ? '../resources/icon/icon-trash-active.svg'
: '../resources/icon/icon-trash-sidenav.svg' : '../resources/icon/icon-trash-sidenav.svg'
} }
/> />
</div> </div>
<span styleName='menu-button-label'>Trash</span> <span styleName='menu-button-label'>{i18n.__('Trash')}</span>
<span styleName='counters'>{counterDelNote}</span> <span styleName='counters'>{counterDelNote}</span>
</button> </button>

View File

@@ -68,10 +68,9 @@
.menu-button-label .menu-button-label
position fixed position fixed
display inline-block display inline-block
height 32px height 36px
left 44px left 44px
padding 0 10px padding 0 10px
margin-top -8px
margin-left 0 margin-left 0
overflow ellipsis overflow ellipsis
z-index 10 z-index 10
@@ -223,3 +222,45 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
.menu-button-label .menu-button-label
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
body[data-theme="monokai"]
.menu-button
&: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
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-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

View File

@@ -2,6 +2,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetTab.styl' import styles from './SnippetTab.styl'
import context from 'browser/lib/context' import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
class SnippetTab extends React.Component { class SnippetTab extends React.Component {
constructor (props) { constructor (props) {
@@ -28,7 +29,7 @@ class SnippetTab extends React.Component {
handleContextMenu (e) { handleContextMenu (e) {
context.popup([ context.popup([
{ {
label: 'Rename', label: i18n.__('Rename'),
click: (e) => this.handleRenameClick(e) click: (e) => this.handleRenameClick(e)
} }
]) ])
@@ -54,10 +55,10 @@ class SnippetTab extends React.Component {
this.handleRename() this.handleRename()
break break
case 27: case 27:
this.setState({ this.setState((prevState, props) => ({
name: this.props.snippet.name, name: props.snippet.name,
isRenaming: false isRenaming: false
}) }))
break break
} }
} }
@@ -114,7 +115,7 @@ class SnippetTab extends React.Component {
{snippet.name.trim().length > 0 {snippet.name.trim().length > 0
? snippet.name ? snippet.name
: <span styleName='button-unnamed'> : <span styleName='button-unnamed'>
Unnamed {i18n.__('Unnamed')}
</span> </span>
} }
</button> </button>

View File

@@ -1,6 +1,7 @@
.root .root
position relative position relative
flex 1 flex 1
min-width 70px
overflow hidden overflow hidden
&:hover &:hover
.deleteButton .deleteButton
@@ -21,7 +22,7 @@
height 29px height 29px
overflow ellipsis overflow ellipsis
text-align left text-align left
padding-right 30px padding-right 23px
border none border none
background-color transparent background-color transparent
transition 0.15s transition 0.15s
@@ -136,3 +137,47 @@ body[data-theme="solarized-dark"]
.deleteButton .deleteButton
color alpha($ui-solarized-dark-text-color, 30%) color alpha($ui-solarized-dark-text-color, 30%)
body[data-theme="monokai"]
.root
color $ui-monokai-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-monokai-noteDetail-backgroundColor
.deleteButton
color $ui-monokai-text-color
&:hover
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
&:active
color $ui-monokai-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-monokai-text-color
border-color $ui-monokai-borderColor
&:hover
background-color $ui-monokai-noteDetail-backgroundColor
.deleteButton
color $ui-monokai-text-color
&:hover
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
&:active
color $ui-monokai-text-color
background-color $ui-dark-button--active-backgroundColor
.button
border none
color $ui-monokai-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-noteDetail-backgroundColor
.input
background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color
.deleteButton
color alpha($ui-monokai-text-color, 30%)

View File

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

View File

@@ -13,6 +13,7 @@
border none border none
overflow ellipsis overflow ellipsis
font-size 14px font-size 14px
align-items: center
&:first-child &:first-child
margin-top 0 margin-top 0
&:hover &:hover
@@ -34,9 +35,7 @@
.folderList-item-name .folderList-item-name
display block display block
flex 1 flex 1
padding 0 12px padding-right: 10px
height 26px
line-height 26px
border-width 0 0 0 2px border-width 0 0 0 2px
border-style solid border-style solid
border-color transparent border-color transparent
@@ -59,8 +58,8 @@
opacity 0 opacity 0
border-top-right-radius 2px border-top-right-radius 2px
border-bottom-right-radius 2px border-bottom-right-radius 2px
height 26px height 34px
line-height 26px line-height 32px
.folderList-item:hover, .folderList-item--active:hover .folderList-item:hover, .folderList-item--active:hover
.folderList-item-tooltip .folderList-item-tooltip
@@ -69,9 +68,20 @@
.folderList-item-name--folded .folderList-item-name--folded
@extend .folderList-item-name @extend .folderList-item-name
padding-left 7px padding-left 7px
text .folderList-item-icon
font-size 9px font-size 9px
.folderList-item-icon
padding-right: 10px
.folderList-item-reorder
font-size: 9px
padding: 10px 8px 10px 9px;
color: rgba(147, 147, 149, 0.3)
cursor: ns-resize
&:before
content: "\f142 \f142"
body[data-theme="white"] body[data-theme="white"]
.folderList-item .folderList-item
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -128,3 +138,22 @@ body[data-theme="solarized-dark"]
&:hover &:hover
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-button-backgroundColor background-color $ui-solarized-dark-button-backgroundColor
body[data-theme="monokai"]
.folderList-item
&: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
@extend .folderList-item
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

View File

@@ -10,8 +10,8 @@ import CSSModules from 'browser/lib/CSSModules'
* @param {Array} storgaeList * @param {Array} storgaeList
*/ */
const StorageList = ({storageList}) => ( const StorageList = ({storageList, isFolded}) => (
<div styleName='storageList'> <div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
{storageList.length > 0 ? storageList : ( {storageList.length > 0 ? storageList : (
<div styleName='storgaeList-empty'>No storage mount.</div> <div styleName='storgaeList-empty'>No storage mount.</div>
)} )}

View File

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

View File

@@ -9,15 +9,26 @@ import CSSModules from 'browser/lib/CSSModules'
/** /**
* @param {string} name * @param {string} name
* @param {Function} handleClickTagListItem * @param {Function} handleClickTagListItem
* @param {Function} handleClickNarrowToTag
* @param {bool} isActive * @param {bool} isActive
* @param {bool} isRelated
*/ */
const TagListItem = ({name, handleClickTagListItem, isActive}) => ( const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}> <div styleName='tagList-itemContainer'>
<span styleName='tagList-item-name'> {isRelated
{`# ${name}`} ? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
</span> <i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
</button> </button>
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
}
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
<span styleName='tagList-item-name'>
{`# ${name}`}
<span styleName='tagList-item-count'>{count}</span>
</span>
</button>
</div>
) )
TagListItem.propTypes = { TagListItem.propTypes = {

View File

@@ -1,5 +1,9 @@
.tagList-itemContainer
display flex
.tagList-item .tagList-item
display flex display flex
flex 1
width 100% width 100%
height 26px height 26px
background-color transparent background-color transparent
@@ -20,9 +24,16 @@
color $ui-button-default-color color $ui-button-default-color
background-color $ui-button-default--active-backgroundColor background-color $ui-button-default--active-backgroundColor
.tagList-itemNarrow
composes tagList-item
flex none
width 20px
padding 0 4px
.tagList-item-active .tagList-item-active
background-color $ui-button-default--active-backgroundColor background-color $ui-button-default--active-backgroundColor
display flex display flex
flex 1
width 100% width 100%
height 26px height 26px
padding 0 padding 0
@@ -36,10 +47,16 @@
background-color alpha($ui-button-default--active-backgroundColor, 60%) background-color alpha($ui-button-default--active-backgroundColor, 60%)
transition 0.2s transition 0.2s
.tagList-itemNarrow-active
composes tagList-item-active
flex none
width 20px
padding 0 4px
.tagList-item-name .tagList-item-name
display block display block
flex 1 flex 1
padding 0 15px padding 0 8px 0 4px
height 26px height 26px
line-height 26px line-height 26px
border-width 0 0 0 2px border-width 0 0 0 2px
@@ -48,6 +65,12 @@
overflow hidden overflow hidden
text-overflow ellipsis text-overflow ellipsis
.tagList-item-count
float right
line-height 26px
padding-right 15px
font-size 13px
body[data-theme="white"] body[data-theme="white"]
.tagList-item .tagList-item
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -63,6 +86,8 @@ body[data-theme="white"]
color $ui-text-color color $ui-text-color
&:hover &:hover
background-color alpha($ui-button--active-backgroundColor, 60%) background-color alpha($ui-button--active-backgroundColor, 60%)
.tagList-item-count
color $ui-text-color
body[data-theme="dark"] body[data-theme="dark"]
.tagList-item .tagList-item
@@ -82,3 +107,5 @@ body[data-theme="dark"]
&:hover &:hover
color $ui-dark-text-color color $ui-dark-text-color
background-color alpha($ui-dark-button--active-backgroundColor, 50%) background-color alpha($ui-dark-button--active-backgroundColor, 50%)
.tagList-item-count
color $ui-dark-button--active-color

View File

@@ -1,6 +1,6 @@
.percentageBar .percentageBar
position absolute position absolute
top 50px top 72px
right 0px right 0px
left 0px left 0px
background-color #DADFE1 background-color #DADFE1
@@ -49,3 +49,13 @@ body[data-theme="solarized-dark"]
.percentageText .percentageText
color #fdf6e3 color #fdf6e3
body[data-theme="monokai"]
.percentageBar
background-color #f92672
.progressBar
background-color: #373831
.percentageText
color #fdf6e3

View File

@@ -58,7 +58,7 @@ body
.katex .katex
font 400 1.2em 'KaTeX_Main' font 400 1.2em 'KaTeX_Main'
line-height 1.2em line-height 1.2em
white-space nowrap white-space initial
text-indent 0 text-indent 0
.katex .mfrac>.vlist>span:nth-child(2) .katex .mfrac>.vlist>span:nth-child(2)
top 0 !important top 0 !important
@@ -68,7 +68,7 @@ body
padding 5px padding 5px
margin -5px margin -5px
border-radius 5px border-radius 5px
.flowchart-error, .sequence-error .flowchart-error, .sequence-error .chart-error
background-color errorBackgroundColor background-color errorBackgroundColor
color errorTextColor color errorTextColor
padding 5px padding 5px
@@ -76,7 +76,7 @@ body
justify-content left justify-content left
li li
label.taskListItem label.taskListItem
margin-left -2em margin-left -1.8em
&.checked &.checked
text-decoration line-through text-decoration line-through
opacity 0.5 opacity 0.5
@@ -178,6 +178,8 @@ ul
margin-bottom 1em margin-bottom 1em
li li
display list-item display list-item
&.taskListItem
list-style none
p p
margin 0 margin 0
&>li>ul, &>li>ol &>li>ul, &>li>ol
@@ -197,7 +199,6 @@ ol
&>li>ul, &>li>ol &>li>ul, &>li>ol
margin 0 margin 0
code code
color #CC305F
padding 0.2em 0.4em padding 0.2em 0.4em
background-color #f7f7f7 background-color #f7f7f7
border-radius 3px border-radius 3px
@@ -212,12 +213,13 @@ pre
margin 0 0 1em margin 0 0 1em
display flex display flex
line-height 1.4em line-height 1.4em
&.flowchart, &.sequence &.flowchart, &.sequence, &.chart
display flex display flex
justify-content center justify-content center
background-color white background-color white
&.CodeMirror &.CodeMirror
height initial height initial
flex-wrap wrap
&>code &>code
flex 1 flex 1
overflow-x auto overflow-x auto
@@ -227,6 +229,13 @@ pre
padding 0 padding 0
border none border none
border-radius 0 border-radius 0
&>span.filename
width 100%
border-radius: 5px 0px 0px 0px
margin -8px 100% 8px -8px
padding 0px 6px
background-color #777;
color white
&>span.lineNumber &>span.lineNumber
display none display none
font-size 1em font-size 1em
@@ -248,6 +257,7 @@ table
display block display block
width 100% width 100%
margin 0 0 1em margin 0 0 1em
overflow-x auto
thead thead
tr tr
background-color tableHeadBgColor background-color tableHeadBgColor
@@ -284,6 +294,84 @@ kbd
line-height 1 line-height 1
padding 3px 5px padding 3px 5px
$admonition
box-shadow 0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)
position relative
margin 1.5625em 0
padding 0 1.2rem
border-left .4rem solid #448aff
border-radius .2rem
overflow auto
html .admonition>:last-child
margin-bottom 1.2rem
.admonition .admonition
margin 1em 0
.admonition p
margin-top: 0.5em
$admonition-icon
position absolute
left 1.2rem
font-family: "Material Icons"
font-weight: normal;
font-style: normal;
font-size: 24px
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
$admonition-title
margin 0 -1.2rem
padding .8rem 1.2rem .8rem 4rem
border-bottom .1rem solid rgba(68,138,255,.1)
background-color rgba(68,138,255,.1)
font-weight 700
.admonition>.admonition-title:last-child
margin-bottom 0
admonition_types = {
note: {color: #0288D1, icon: "note"},
hint: {color: #009688, icon: "info_outline"},
danger: {color: #c2185b, icon: "block"},
caution: {color: #ffa726, icon: "warning"},
error: {color: #d32f2f, icon: "error_outline"},
attention: {color: #455a64, icon: "priority_high"}
}
for name, val in admonition_types
.admonition.{name}
@extend $admonition
border-left-color: val[color]
.admonition.{name}>.admonition-title
@extend $admonition-title
border-bottom-color: .1rem solid rgba(val[color], 0.2)
background-color: rgba(val[color], 0.2)
.admonition.{name}>.admonition-title:before
@extend $admonition-icon
color: val[color]
content: val[icon]
themeDarkBackground = darken(#21252B, 10%) themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9 themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%) themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -361,3 +449,32 @@ body[data-theme="solarized-dark"]
border-color themeSolarizedDarkTableBorder border-color themeSolarizedDarkTableBorder
&:last-child &:last-child
border-right solid 1px themeSolarizedDarkTableBorder border-right solid 1px themeSolarizedDarkTableBorder
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
themeMonokaiTableHead = themeMonokaiTableEven
themeMonokaiTableBorder = themeDarkBorder
body[data-theme="monokai"]
color $ui-monokai-text-color
border-color themeDarkBorder
background-color $ui-monokai-noteDetail-backgroundColor
table
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

View File

@@ -0,0 +1,39 @@
import mermaidAPI from 'mermaid'
// fixes bad styling in the mermaid dark theme
const darkThemeStyling = `
.loopText tspan {
fill: white;
}`
function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min)) + min
}
function getId () {
var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
var id = 'm-'
for (var i = 0; i < 7; i++) {
id += pool[getRandomInt(0, 16)]
}
return id
}
function render (element, content, theme) {
try {
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai'
mermaidAPI.initialize({
theme: isDarkTheme ? 'dark' : 'default',
themeCSS: isDarkTheme ? darkThemeStyling : ''
})
mermaidAPI.render(getId(), content, (svgGraph) => {
element.innerHTML = svgGraph
})
} catch (e) {
console.error(e)
element.className = 'mermaid-error'
element.innerHTML = 'mermaid diagram parse error: ' + e.message
}
}
export default render

View File

@@ -1,156 +0,0 @@
$search-height = 50px
$nav-width = 175px
$list-width = 250px
.root
absolute top left right bottom
.search
height $search-height
padding 10px
box-sizing border-box
border-bottom $ui-border
text-align center
.search-input
height 30px
width 100%
margin 0 auto
font-size 18px
border none
outline none
text-align center
background-color transparent
.result
absolute left right bottom
top $search-height
background-color $ui-noteDetail-backgroundColor
.result-nav
user-select none
absolute left top bottom
width $nav-width
background-color $ui-backgroundColor
.result-nav-filter
margin-bottom 10px
.result-nav-filter-option
height 25px
line-height 25px
padding 0 10px
label
cursor pointer
.result-nav-menu
navButtonColor()
height 32px
padding 0 10px
font-size 14px
width 100%
outline none
text-align left
line-height 32px
box-sizing border-box
cursor pointer
.result-nav-menu--active
@extend .result-nav-menu
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.result-nav-storageList
absolute bottom left right
top 110px + 32px + 10px + 10px + 20px
overflow-y auto
.result-list
user-select none
absolute top bottom
left $nav-width
width $list-width
box-sizing border-box
overflow-y auto
box-shadow 2px 0 15px -8px #b1b1b1
z-index 1
.result-detail
absolute top bottom right
left $nav-width + $list-width
background-color $ui-noteDetail-backgroundColor
body[data-theme="dark"]
.root
background-color $ui-dark-backgroundColor
.search
border-color $ui-dark-borderColor
.search-input
color $ui-dark-text-color
.result
background-color $ui-dark-noteList-backgroundColor
.result-nav
background-color $ui-dark-backgroundColor
label
color $ui-dark-text-color
.result-nav-menu
navDarkButtonColor()
.result-nav-menu--active
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-button--active-color
&:hover
background-color $ui-dark-button--active-backgroundColor
.result-list
border-color $ui-dark-borderColor
box-shadow none
top 0
.result-detail
absolute top bottom right
left $nav-width + $list-width
background-color $ui-dark-noteDetail-backgroundColor
body[data-theme="solarized-dark"]
.root
background-color $ui-solarized-dark-backgroundColor
.search
border-color $ui-solarized-dark-borderColor
.search-input
color $ui-dark-text-color
.result
background-color $ui-solarized-dark-backgroundColor
.result-nav
background-color $ui-solarized-dark-backgroundColor
label
color $ui-dark-text-color
.result-nav-menu
navDarkButtonColor()
.result-nav-menu--active
background-color $ui-solarized-dark-button-backgroundColor
color $ui-dark-button--active-color
&:hover
background-color $ui-dark-button--active-backgroundColor
.result-list
border-color $ui-solarized-dark-borderColor
box-shadow none
top 0
.result-detail
absolute top bottom right
left $nav-width + $list-width
background-color $ui-solarized-dark-backgroundColor

View File

@@ -1,211 +0,0 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteDetail.styl'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import CodeEditor from 'browser/components/CodeEditor'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
import { findStorage } from 'browser/lib/findStorage'
const electron = require('electron')
const { clipboard } = electron
const path = require('path')
function pass (name) {
switch (name) {
case 'ejs':
return 'Embedded Javascript'
case 'html_ruby':
return 'Embedded Ruby'
case 'objectivec':
return 'Objective C'
case 'text':
return 'Plain Text'
default:
return name
}
}
function notify (title, options) {
if (global.process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
}
return new window.Notification(title, options)
}
class NoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
snippetIndex: 0
}
}
componentWillReceiveProps (nextProps) {
if (nextProps.note !== this.props.note) {
this.setState({
snippetIndex: 0
}, () => {
if (nextProps.note.type === 'SNIPPET_NOTE') {
nextProps.note.snippets.forEach((snippet, index) => {
this.refs['code-' + index].reload()
})
}
})
}
}
selectPriorSnippet () {
const { note } = this.props
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
this.setState({
snippetIndex: (this.state.snippetIndex + note.snippets.length - 1) % note.snippets.length
})
}
}
selectNextSnippet () {
const { note } = this.props
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
this.setState({
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
})
}
}
saveToClipboard () {
const { note } = this.props
if (note.type === 'MARKDOWN_NOTE') {
clipboard.writeText(note.content)
} else {
clipboard.writeText(note.snippets[this.state.snippetIndex].content)
}
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
}
handleTabButtonClick (e, index) {
this.setState({
snippetIndex: index
})
}
render () {
const { note, config } = this.props
if (note == null) {
return (
<div styleName='root' />
)
}
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
const storage = findStorage(note.storage)
if (note.type === 'SNIPPET_NOTE') {
const tabList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index
return <div styleName={isActive
? 'tabList-item--active'
: 'tabList-item'
}
key={index}
>
<button styleName='tabList-item-button'
onClick={(e) => this.handleTabButtonClick(e, index)}
>
{snippet.name.trim().length > 0
? snippet.name
: <span styleName='tabList-item-unnamed'>
Unnamed
</span>
}
</button>
</div>
})
const viewList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
return <div styleName='tabView'
key={index}
style={{zIndex: isActive ? 5 : 4}}
>
{snippet.mode === 'markdown'
? <MarkdownEditor styleName='tabView-content'
config={config}
value={snippet.content}
ref={'code-' + index}
storageKey={note.storage}
/>
: <CodeEditor styleName='tabView-content'
mode={snippet.mode}
value={snippet.content}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd}
readOnly
ref={'code-' + index}
/>
}
</div>
})
return (
<div styleName='root'>
<div styleName='description'>
<textarea styleName='description-textarea'
style={{
fontFamily: config.preview.fontFamily,
fontSize: parseInt(config.preview.fontSize, 10)
}}
ref='description'
placeholder='Description...'
value={note.description}
readOnly
/>
</div>
<div styleName='tabList'>
{tabList}
</div>
{viewList}
</div>
)
}
return (
<MarkdownPreview styleName='root'
theme={config.ui.theme}
fontSize={config.preview.fontSize}
fontFamily={config.preview.fontFamily}
codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
value={note.content}
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path}
/>
)
}
}
NoteDetail.propTypes = {
}
export default CSSModules(NoteDetail, styles)

View File

@@ -1,129 +0,0 @@
@import('../main/Detail/DetailVars.styl')
.root
absolute top bottom left right
bottom 30px
margin 0 25px
height 100%
width 365px
background-color $ui-noteDetail-backgroundColor
.description
absolute top left right
height 80px
box-sizing border-box
.description-textarea
display block
height 100%
width 100%
resize none
border none
padding 10px
line-height 1.6
box-sizing border-box
background-color $ui-noteDetail-backgroundColor
.tabList
absolute left right
top 80px
box-sizing border-box
height 30px
display flex
background-color $ui-noteDetail-backgroundColor
.tabList-item
position relative
flex 1
overflow hidden
&:hover
background-color $ui-button--hover-backgroundColorg
.tabList-item--active
@extend .tabList-item
border-bottom $ui-border
.tabList-item-button
width 100%
height 29px
overflow ellipsis
text-align left
padding-right 30px
padding-left 10px
border none
background-color transparent
transition 0.15s
&:hover
background-color $ui-button--hover-backgroundColor
.tabView
absolute left right bottom
top 130px
.tabView-content
absolute top left right bottom
box-sizing border-box
height 100%
width 100%
body[data-theme="dark"]
.root
background-color $ui-dark-noteDetail-backgroundColor
.description
border-color $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
.description-textarea
background-color $ui-dark-noteDetail-backgroundColor
color white
.tabList
background-color $ui-dark-noteDetail-backgroundColor
.tabList-item
border-color $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
.tabList-item-button
border none
color $ui-dark-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color white
background-color $ui-dark-button--hover-backgroundColor
body[data-theme="solarized-dark"]
.root
background-color $ui-solarized-dark-backgroundColor
.description
border-color $ui-dark-borderColor
background-color $ui-solarized-dark-backgroundColor
.description-textarea
background-color $ui-solarized-dark-backgroundColor
color white
.tabList
background-color $ui-solarized-dark-backgroundColor
.tabList-item
border-color $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
.tabList-item-button
border none
color $ui-dark-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color white
background-color $ui-dark-button--hover-backgroundColor

View File

@@ -1,90 +0,0 @@
import React from 'react'
import NoteItem from 'browser/components/NoteItem'
import moment from 'moment'
class NoteList extends React.Component {
constructor (props) {
super(props)
this.state = {
range: 0
}
}
componentWillReceiveProps (nextProps) {
if (this.props.search !== nextProps.search) {
this.resetScroll()
}
}
componentDidUpdate () {
const { index } = this.props
if (index > -1) {
const list = this.refs.root
const item = list.childNodes[index]
if (item == null) return null
const overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
if (overflowBelow) {
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
}
const overflowAbove = list.scrollTop > item.offsetTop
if (overflowAbove) {
list.scrollTop = item.offsetTop
}
}
}
resetScroll () {
this.refs.root.scrollTop = 0
this.setState({
range: 0
})
}
handleScroll (e) {
const { notes } = this.props
if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 100 && notes.length > this.state.range * 10 + 10) {
this.setState({
range: this.state.range + 1
})
}
}
render () {
const { notes, index } = this.props
const notesList = notes
.slice(0, 10 + 10 * this.state.range)
.map((note, _index) => {
const isActive = (index === _index)
const key = `${note.storage}-${note.key}`
const dateDisplay = moment(note.updatedAt).fromNow()
return (
<NoteItem
isActive={isActive}
note={note}
dateDisplay={dateDisplay}
key={key}
handleNoteClick={(e) => this.props.handleNoteClick(e, _index)}
/>
)
})
return (
<div className={this.props.className}
onScroll={(e) => this.handleScroll(e)}
ref='root'
>
{notesList}
</div>
)
}
}
NoteList.propTypes = {
}
export default NoteList

View File

@@ -1,77 +0,0 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageSection.styl'
import StorageItem from 'browser/components/StorageItem'
class StorageSection extends React.Component {
constructor (props) {
super(props)
this.state = {
isOpen: true
}
}
handleToggleButtonClick (e) {
this.setState({
isOpen: !this.state.isOpen
})
}
handleHeaderClick (e) {
const { storage } = this.props
this.props.handleStorageButtonClick(e, storage.key)
}
handleFolderClick (e, folder) {
const { storage } = this.props
this.props.handleFolderButtonClick(e, storage.key, folder.key)
}
render () {
const { storage, filter } = this.props
const folderList = storage.folders
.map(folder => (
<StorageItem
key={folder.key}
isActive={filter.type === 'FOLDER' && filter.folder === folder.key && filter.storage === storage.key}
handleButtonClick={(e) => this.handleFolderClick(e, folder)}
folderName={folder.name}
folderColor={folder.color}
isFolded={false}
/>
))
return (
<div styleName='root'>
<div styleName='header'>
<button styleName='header-toggleButton'
onClick={(e) => this.handleToggleButtonClick(e)}
>
<i className={this.state.isOpen
? 'fa fa-caret-down'
: 'fa fa-caret-right'
}
/>
</button>
<button styleName={filter.type === 'STORAGE' && filter.storage === storage.key
? 'header-name--active'
: 'header-name'
}
onClick={(e) => this.handleHeaderClick(e)}
>{storage.name}</button>
</div>
{this.state.isOpen &&
<div styleName='folderList'>
{folderList}
</div>
}
</div>
)
}
}
StorageSection.propTypes = {
}
export default CSSModules(StorageSection, styles)

View File

@@ -1,85 +0,0 @@
.root
position relative
.header
height 26px
.header-toggleButton
absolute top left
width 25px
height 26px
navButtonColor()
border none
outline none
.header-name
display block
height 26px
navButtonColor()
padding 0 10px 0 25px
font-size 14px
width 100%
text-align left
line-height 26px
box-sizing border-box
cursor pointer
outline none
.header-name--active
@extend .header-name
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.folderList-item
display block
width 100%
height 26px
navButtonColor()
padding 0 10px 0 25px
font-size 14px
width 100%
text-align left
line-height 26px
box-sizing border-box
cursor pointer
outline none
padding 0 10px
margin 2px 0
height 26px
line-height 26px
border-width 0 0 0 6px
border-style solid
border-color transparent
.folderList-item--active
@extend .folderList-item
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
body[data-theme="dark"]
.header-toggleButton
navDarkButtonColor()
.header-name
navDarkButtonColor()
.header-name--active
@extend .header-name
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.folderList-item
navDarkButtonColor()
border-width 0 0 0 6px
border-style solid
border-color transparent
.folderList-item--active
@extend .folderList-item
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor

View File

@@ -1,357 +0,0 @@
import PropTypes from 'prop-types'
import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import _ from 'lodash'
import store from './store'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FinderMain.styl'
import StorageSection from './StorageSection'
import NoteList from './NoteList'
import NoteDetail from './NoteDetail'
import SideNavFilter from 'browser/components/SideNavFilter'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
require('!!style!css!stylus?sourceMap!../main/global.styl')
require('../lib/customMeta')
require('./ipcClient.js')
const electron = require('electron')
const { remote } = electron
const { Menu } = remote
function hideFinder () {
const finderWindow = remote.getCurrentWindow()
if (global.process.platform === 'win32') {
finderWindow.blur()
finderWindow.hide()
}
if (global.process.platform === 'darwin') {
Menu.sendActionToFirstResponder('hide:')
}
remote.getCurrentWindow().hide()
}
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
class FinderMain extends React.Component {
constructor (props) {
super(props)
this.state = {
search: '',
index: 0,
filter: {
includeSnippet: true,
includeMarkdown: false,
type: 'ALL',
storage: null,
folder: null
}
}
this.focusHandler = (e) => this.handleWindowFocus(e)
this.blurHandler = (e) => this.handleWindowBlur(e)
}
componentDidMount () {
this.refs.search.focus()
window.addEventListener('focus', this.focusHandler)
window.addEventListener('blur', this.blurHandler)
}
componentWillUnmount () {
window.removeEventListener('focus', this.focusHandler)
window.removeEventListener('blur', this.blurHandler)
}
handleWindowFocus (e) {
this.refs.search.focus()
}
handleWindowBlur (e) {
this.setState({
search: ''
})
}
handleKeyDown (e) {
this.refs.search.focus()
if (e.keyCode === 9) {
if (e.shiftKey) {
this.refs.detail.selectPriorSnippet()
} else {
this.refs.detail.selectNextSnippet()
}
e.preventDefault()
}
if (e.keyCode === 38) {
this.selectPrevious()
e.preventDefault()
}
if (e.keyCode === 40) {
this.selectNext()
e.preventDefault()
}
if (e.keyCode === 13) {
this.refs.detail.saveToClipboard()
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('COPY_FINDER')
hideFinder()
e.preventDefault()
}
if (e.keyCode === 27) {
hideFinder()
e.preventDefault()
}
if (e.keyCode === 91 || e.metaKey) {
return
}
}
handleSearchChange (e) {
this.setState({
search: e.target.value,
index: 0
})
}
selectArticle (article) {
this.setState({currentArticle: article})
}
selectPrevious () {
if (this.state.index > 0) {
this.setState({
index: this.state.index - 1
})
}
}
selectNext () {
if (this.state.index < this.noteCount - 1) {
this.setState({
index: this.state.index + 1
})
}
}
handleOnlySnippetCheckboxChange (e) {
const { filter } = this.state
filter.includeSnippet = e.target.checked
this.setState({
filter: filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleOnlyMarkdownCheckboxChange (e) {
const { filter } = this.state
filter.includeMarkdown = e.target.checked
this.refs.list.resetScroll()
this.setState({
filter: filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleAllNotesButtonClick (e) {
const { filter } = this.state
filter.type = 'ALL'
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleStarredButtonClick (e) {
const { filter } = this.state
filter.type = 'STARRED'
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleStorageButtonClick (e, storage) {
const { filter } = this.state
filter.type = 'STORAGE'
filter.storage = storage
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleFolderButtonClick (e, storage, folder) {
const { filter } = this.state
filter.type = 'FOLDER'
filter.storage = storage
filter.folder = folder
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleNoteClick (e, index) {
this.setState({
index
}, () => {
this.refs.search.focus()
})
}
render () {
const { data, config } = this.props
const { filter, search } = this.state
const storageList = []
for (const key in data.storageMap) {
const storage = data.storageMap[key]
const item = (
<StorageSection
filter={filter}
storage={storage}
key={storage.key}
handleStorageButtonClick={(e, storage) => this.handleStorageButtonClick(e, storage)}
handleFolderButtonClick={(e, storage, folder) => this.handleFolderButtonClick(e, storage, folder)}
/>
)
storageList.push(item)
}
let notes = []
let noteIds
switch (filter.type) {
case 'STORAGE':
noteIds = data.storageNoteMap[filter.storage]
break
case 'FOLDER':
noteIds = data.folderNoteMap[filter.storage + '-' + filter.folder]
break
case 'STARRED':
noteIds = data.starredSet
}
if (noteIds != null) {
noteIds.forEach((id) => {
notes.push(data.noteMap[id])
})
} else {
for (const key in data.noteMap) {
notes.push(data.noteMap[key])
}
}
if (!filter.includeSnippet && filter.includeMarkdown) {
notes = notes.filter((note) => note.type === 'MARKDOWN_NOTE')
} else if (filter.includeSnippet && !filter.includeMarkdown) {
notes = notes.filter((note) => note.type === 'SNIPPET_NOTE')
}
if (search.trim().length > 0) {
const needle = new RegExp(_.escapeRegExp(search.trim()), 'i')
notes = notes.filter((note) => note.title.match(needle))
}
notes = notes
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
const activeNote = notes[this.state.index]
this.noteCount = notes.length
return (
<div className='Finder'
styleName='root'
ref='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='search'>
<input
styleName='search-input'
ref='search'
value={search}
placeholder='Search...'
onChange={(e) => this.handleSearchChange(e)}
/>
</div>
<div styleName='result'>
<div styleName='result-nav'>
<div styleName='result-nav-filter'>
<div styleName='result-nav-filter-option'>
<label>
<input type='checkbox'
checked={filter.includeSnippet}
onChange={(e) => this.handleOnlySnippetCheckboxChange(e)}
/> Only Snippets</label>
</div>
<div styleName='result-nav-filter-option'>
<label>
<input type='checkbox'
checked={filter.includeMarkdown}
onChange={(e) => this.handleOnlyMarkdownCheckboxChange(e)}
/> Only Markdown</label>
</div>
</div>
<SideNavFilter
isHomeActive={filter.type === 'ALL'}
handleAllNotesButtonClick={(e) => this.handleAllNotesButtonClick(e)}
isStarredActive={filter.type === 'STARRED'}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
/>
<div styleName='result-nav-storageList'>
{storageList}
</div>
</div>
<NoteList styleName='result-list'
storageMap={data.storageMap}
notes={notes}
ref='list'
search={search}
index={this.state.index}
handleNoteClick={(e, _index) => this.handleNoteClick(e, _index)}
/>
<div styleName='result-detail'>
<NoteDetail
note={activeNote}
config={config}
ref='detail'
/>
</div>
</div>
</div>
)
}
}
FinderMain.propTypes = {
dispatch: PropTypes.func
}
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
function refreshData () {
// let data = dataStore.getData(true)
}
ReactDOM.render((
<Provider store={store}>
<Finder />
</Provider>
), document.getElementById('content'), function () {
refreshData()
})

View File

@@ -1,126 +0,0 @@
const nodeIpc = require('node-ipc')
const { remote, ipcRenderer } = require('electron')
const { app, Menu } = remote
const path = require('path')
const store = require('./store')
const consts = require('browser/lib/consts')
nodeIpc.config.id = 'finder'
nodeIpc.config.retry = 1500
nodeIpc.config.silent = true
function killFinder () {
const finderWindow = remote.getCurrentWindow()
finderWindow.removeAllListeners()
if (global.process.platform === 'darwin') {
// Only OSX has another app process.
nodeIpc.of.node.emit('quit-from-finder')
} else {
finderWindow.close()
}
}
function toggleFinder () {
const finderWindow = remote.getCurrentWindow()
if (global.process.platform === 'darwin') {
if (finderWindow.isVisible()) {
finderWindow.hide()
Menu.sendActionToFirstResponder('hide:')
} else {
nodeIpc.of.node.emit('request-data-from-finder')
finderWindow.show()
}
} else {
if (finderWindow.isVisible()) {
finderWindow.blur()
finderWindow.hide()
} else {
nodeIpc.of.node.emit('request-data-from-finder')
finderWindow.show()
finderWindow.focus()
}
}
}
nodeIpc.connectTo(
'node',
path.join(app.getPath('userData'), 'boostnote.service'),
function () {
nodeIpc.of.node.on('error', function (err) {
console.log(err)
})
nodeIpc.of.node.on('connect', function () {
console.log('Conncted successfully')
})
nodeIpc.of.node.on('disconnect', function () {
console.log('disconnected')
})
nodeIpc.of.node.on('open-finder', function () {
toggleFinder()
})
ipcRenderer.on('open-finder-from-tray', function () {
toggleFinder()
})
ipcRenderer.on('open-main-from-tray', function () {
nodeIpc.of.node.emit('open-main-from-finder')
})
ipcRenderer.on('quit-from-tray', function () {
nodeIpc.of.node.emit('quit-from-finder')
killFinder()
})
nodeIpc.of.node.on('throttle-data', function (payload) {
console.log('Received data from Main renderer')
store.default.dispatch({
type: 'THROTTLE_DATA',
data: payload
})
})
nodeIpc.of.node.on('config-renew', function (payload) {
const { config } = payload
if (config.ui.theme === 'dark') {
document.body.setAttribute('data-theme', 'dark')
} else if (config.ui.theme === 'white') {
document.body.setAttribute('data-theme', 'white')
} else if (config.ui.theme === 'solarized-dark') {
document.body.setAttribute('data-theme', 'solarized-dark')
} else {
document.body.setAttribute('data-theme', 'default')
}
let editorTheme = document.getElementById('editorTheme')
if (editorTheme == null) {
editorTheme = document.createElement('link')
editorTheme.setAttribute('id', 'editorTheme')
editorTheme.setAttribute('rel', 'stylesheet')
document.head.appendChild(editorTheme)
}
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
? config.editor.theme
: 'default'
if (config.editor.theme !== 'default') {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
}
store.default.dispatch({
type: 'SET_CONFIG',
config: config
})
})
nodeIpc.of.node.on('quit-finder-app', function () {
nodeIpc.of.node.emit('quit-finder-app-confirm')
killFinder()
})
}
)
const ipc = {}
module.exports = ipc

View File

@@ -1,51 +0,0 @@
import { combineReducers, createStore } from 'redux'
import { routerReducer } from 'react-router-redux'
import { DEFAULT_CONFIG } from 'browser/main/lib/ConfigManager'
const defaultData = {
storageMap: {},
noteMap: {},
starredSet: [],
storageNoteMap: {},
folderNoteMap: {},
tagNoteMap: {}
}
function data (state = defaultData, action) {
switch (action.type) {
case 'THROTTLE_DATA':
console.log(action)
state = action.data
}
return state
}
function config (state = DEFAULT_CONFIG, action) {
switch (action.type) {
case 'INIT_CONFIG':
case 'SET_CONFIG':
return Object.assign({}, state, action.config)
case 'SET_IS_SIDENAV_FOLDED':
state.isSideNavFolded = action.isFolded
return Object.assign({}, state)
case 'SET_ZOOM':
state.zoom = action.zoom
return Object.assign({}, state)
case 'SET_LIST_WIDTH':
state.listWidth = action.listWidth
return Object.assign({}, state)
case 'SET_UI':
return Object.assign({}, state, action.config)
}
return state
}
const reducer = combineReducers({
data,
config,
routing: routerReducer
})
const store = createStore(reducer)
export default store

78
browser/lib/Languages.js Normal file
View File

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

View File

@@ -0,0 +1,53 @@
import { Point } from '@susisu/mte-kernel'
export default class TextEditorInterface {
constructor (editor) {
this.editor = editor
}
getCursorPosition () {
const pos = this.editor.getCursor()
return new Point(pos.line, pos.ch)
}
setCursorPosition (pos) {
this.editor.setCursor({line: pos.row, ch: pos.column})
}
setSelectionRange (range) {
this.editor.setSelection({
anchor: {line: range.start.row, ch: range.start.column},
head: {line: range.end.row, ch: range.end.column}
})
}
getLastRow () {
return this.editor.lastLine()
}
acceptsTableEdit (row) {
return true
}
getLine (row) {
return this.editor.getLine(row)
}
insertLine (row, line) {
this.editor.replaceRange(line, {line: row, ch: 0})
}
deleteLine (row) {
this.editor.replaceRange('', {line: row, ch: 0}, {line: row, ch: this.editor.getLine(row).length})
}
replaceLines (startRow, endRow, lines) {
endRow-- // because endRow is a first line after a table.
const endRowCh = this.editor.getLine(endRow).length
this.editor.replaceRange(lines, {line: startRow, ch: 0}, {line: endRow, ch: endRowCh})
}
transact (func) {
func()
}
}

View File

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

View File

@@ -12,6 +12,10 @@ const themes = fs.readdirSync(themePath)
}) })
themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light') themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
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: [
'#E10051', '#E10051',
@@ -31,7 +35,16 @@ const consts = {
'Dodger Blue', 'Dodger Blue',
'Violet Eggplant' 'Violet Eggplant'
], ],
THEMES: ['default'].concat(themes) THEMES: ['default'].concat(themes),
SNIPPET_FILE: snippetFile,
DEFAULT_EDITOR_FONT_FAMILY: [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
} }
module.exports = consts module.exports = consts

View File

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

View File

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

View File

@@ -5,10 +5,10 @@ export function getTodoStatus (content) {
splitted.forEach((line) => { splitted.forEach((line) => {
const trimmedLine = line.trim() const trimmedLine = line.trim()
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) { if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
numberOfTodo++ numberOfTodo++
} }
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) { if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
numberOfCompletedTodo++ numberOfCompletedTodo++
} }
}) })

17
browser/lib/i18n.js Normal file
View File

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

View File

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

View File

@@ -0,0 +1,37 @@
'use strict'
import sanitizeHtml from 'sanitize-html'
import { escapeHtmlCharacters } from './utils'
module.exports = function sanitizePlugin (md, options) {
options = options || {}
md.core.ruler.after('linkify', 'sanitize_inline', state => {
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
if (state.tokens[tokenIdx].type === 'html_block') {
state.tokens[tokenIdx].content = sanitizeHtml(
state.tokens[tokenIdx].content,
options
)
}
if (state.tokens[tokenIdx].type === 'fence') {
// escapeHtmlCharacters has better performance
state.tokens[tokenIdx].content = escapeHtmlCharacters(
state.tokens[tokenIdx].content,
{ skipSingleQuote: true }
)
}
if (state.tokens[tokenIdx].type === 'inline') {
const inlineTokens = state.tokens[tokenIdx].children
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
if (inlineTokens[childIdx].type === 'html_inline') {
inlineTokens[childIdx].content = sanitizeHtml(
inlineTokens[childIdx].content,
options
)
}
}
}
}
})
}

View File

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

0
browser/lib/markdown2.js Normal file
View File

View File

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

View File

@@ -4,39 +4,30 @@ 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 = findByWord(notes, searchBlocks[0]) let foundNotes = notes
searchBlocks.forEach((block) => { searchBlocks.forEach((block) => {
foundNotes = findByWord(foundNotes, block) foundNotes = findByWordOrTag(foundNotes, block)
if (block.match(/^#.+/)) {
foundNotes = foundNotes.concat(findByTag(notes, block))
}
}) })
return foundNotes return foundNotes
} }
function findByTag (notes, block) { function findByWordOrTag (notes, block) {
const tag = block.match(/#(.+)/)[1] let tag = block
const regExp = new RegExp(_.escapeRegExp(tag), 'i') if (tag.match(/^#.+/)) {
tag = tag.match(/#(.+)/)[1]
}
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
return notes.filter((note) => { return notes.filter((note) => {
if (!_.isArray(note.tags)) return false if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) {
return note.tags.some((_tag) => {
return _tag.match(regExp)
})
})
}
function findByWord (notes, block) {
const regExp = new RegExp(_.escapeRegExp(block), 'i')
return notes.filter((note) => {
if (_.isArray(note.tags) && note.tags.some((_tag) => {
return _tag.match(regExp)
})) {
return true return true
} }
if (note.type === 'SNIPPET_NOTE') { if (note.type === 'SNIPPET_NOTE') {
return note.description.match(regExp) return 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(regExp) return note.content.match(wordRegExp)
} }
return false return false
}) })

139
browser/lib/utils.js Normal file
View File

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

View File

@@ -20,11 +20,20 @@
body[data-theme="dark"] body[data-theme="dark"]
.root .root
background-color $ui-dark-backgroundColor background-color $ui-dark-backgroundColor
border-left 1px solid $ui-dark-borderColor
.empty-message .empty-message
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
body[data-theme="solarized-dark"] body[data-theme="solarized-dark"]
.root .root
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color $ui-solarized-dark-noteDetail-backgroundColor
border-left 1px solid $ui-solarized-dark-borderColor
.empty-message .empty-message
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
body[data-theme="monokai"]
.root
background-color $ui-monokai-noteDetail-backgroundColor
border-left 1px solid $ui-monokai-borderColor
.empty-message
color $ui-monokai-text-color

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './FolderSelect.styl' import styles from './FolderSelect.styl'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class FolderSelect extends React.Component { class FolderSelect extends React.Component {
constructor (props) { constructor (props) {
@@ -249,7 +250,7 @@ class FolderSelect extends React.Component {
<input styleName='search-input' <input styleName='search-input'
ref='search' ref='search'
value={this.state.search} value={this.state.search}
placeholder='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)}

View File

@@ -3,20 +3,14 @@
border solid 1px transparent border solid 1px transparent
vertical-align middle vertical-align middle
border-radius 2px border-radius 2px
height 30px
transition 0.15s transition 0.15s
user-select none user-select none
margin-right 10px margin-right 10px
&:hover
background-color $ui-button--hover-backgroundColor
.root--search, .root--focus .root--search, .root--focus
@extend .root @extend .root
background-color $ui-noteDetail-backgroundColor = #fff
border-color $ui-input--focus-borderColor border-color $ui-input--focus-borderColor
width 154px
height 30px
&:hover
border-color $ui-input--focus-borderColor = #fff
.idle .idle
position relative position relative
@@ -139,3 +133,29 @@ body[data-theme="dark"]
color $ui-dark-button--active-color color $ui-dark-button--active-color
.search-optionList-item-name-surfix .search-optionList-item-name-surfix
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
body[data-theme="monokai"]
.root
color $ui-dark-text-color
&:hover
color white
background-color $ui-monokai-button--hover-backgroundColor
border-color $ui-monokai-borderColor
.search-optionList
color white
border-color $ui-monokai-borderColor
background-color $ui-monokai-button-backgroundColor
.search-optionList-item
&:hover
background-color lighten($ui-monokai-button--hover-backgroundColor, 15%)
.search-optionList-item--active
background-color $ui-monokai-button--active-backgroundColor
color $ui-monokai-button--active-color
&: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

View File

@@ -2,13 +2,16 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './FullscreenButton.styl' import styles from './FullscreenButton.styl'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
const FullscreenButton = ({ const FullscreenButton = ({
onClick onClick
}) => ( }) => (
<button styleName='control-fullScreenButton' title='Fullscreen' onMouseDown={(e) => onClick(e)}> <button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' /> <img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>Fullscreen</span> <span styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
</button> </button>
) )

View File

@@ -8,8 +8,8 @@
tooltip() tooltip()
position absolute position absolute
pointer-events none pointer-events none
top 26px top 50px
right 0 right 70px
z-index 200 z-index 200
padding 5px padding 5px
line-height normal line-height normal

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoButton.styl' import styles from './InfoButton.styl'
import i18n from 'browser/lib/i18n'
const InfoButton = ({ const InfoButton = ({
onClick onClick
@@ -10,7 +11,7 @@ const InfoButton = ({
onClick={(e) => onClick(e)} 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'>Info</span> <span styleName='tooltip'>{i18n.__('Info')}</span>
</button> </button>
) )

View File

@@ -8,8 +8,8 @@
tooltip() tooltip()
position absolute position absolute
pointer-events none pointer-events none
top 26px top 50px
right 0 right 20px
z-index 200 z-index 200
padding 5px padding 5px
line-height normal line-height normal

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl' import styles from './InfoPanel.styl'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import i18n from 'browser/lib/i18n'
class InfoPanel extends React.Component { class InfoPanel extends React.Component {
copyNoteLink () { copyNoteLink () {
@@ -19,7 +20,7 @@ class InfoPanel extends React.Component {
<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'>MODIFICATION DATE</p> <p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
</div> </div>
<hr /> <hr />
@@ -29,11 +30,11 @@ class InfoPanel extends React.Component {
: <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'>Words</p> <p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
</div> </div>
<div styleName='count-number'> <div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{letterCount}</p> <p styleName='infoPanel-defaul-count'>{letterCount}</p>
<p styleName='infoPanel-sub-count'>Letters</p> <p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
</div> </div>
</div> </div>
} }
@@ -45,17 +46,17 @@ class InfoPanel extends React.Component {
<div> <div>
<p styleName='infoPanel-default'>{storageName}</p> <p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p> <p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'>{folderName}</p> <p styleName='infoPanel-default'>{folderName}</p>
<p styleName='infoPanel-sub'>FOLDER</p> <p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'>{createdAt}</p> <p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p> <p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
</div> </div>
<div> <div>
@@ -63,7 +64,7 @@ class InfoPanel extends React.Component {
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'> <button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
<i className='fa fa-clipboard' /> <i className='fa fa-clipboard' />
</button> </button>
<p styleName='infoPanel-sub'>NOTE LINK</p> <p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
</div> </div>
<hr /> <hr />
@@ -71,22 +72,22 @@ class InfoPanel extends React.Component {
<div id='export-wrap'> <div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}> <button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<i className='fa fa-file-code-o' /> <i className='fa fa-file-code-o' />
<p>.md</p> <p>{i18n.__('.md')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}> <button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<i className='fa fa-file-text-o' /> <i className='fa fa-file-text-o' />
<p>.txt</p> <p>{i18n.__('.txt')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}> <button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
<i className='fa fa-html5' /> <i className='fa fa-html5' />
<p>.html</p> <p>{i18n.__('.html')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => print(e)}> <button styleName='export--enable' onClick={(e) => print(e)}>
<i className='fa fa-print' /> <i className='fa fa-print' />
<p>Print</p> <p>{i18n.__('Print')}</p>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -11,7 +11,8 @@
.control-infoButton-panel .control-infoButton-panel
z-index 200 z-index 200
margin-top 0px margin-top 0px
right 0 top: 50px
right 25px
position absolute position absolute
padding 20px 25px 0 25px padding 20px 25px 0 25px
width 300px width 300px
@@ -32,6 +33,7 @@
.control-infoButton-panel-trash .control-infoButton-panel-trash
z-index 200 z-index 200
margin-top 0px margin-top 0px
top 50px
right 0px right 0px
position absolute position absolute
padding 20px 25px 0 25px padding 20px 25px 0 25px
@@ -69,15 +71,16 @@
color $ui-text-color color $ui-text-color
.infoPanel-sub .infoPanel-sub
font-size 14px font-size 12px
font-weight 600
color $ui-inactive-text-color color $ui-inactive-text-color
padding-bottom 8px padding-bottom 8px
.infoPanel-noteLink .infoPanel-noteLink
padding-right 5px padding-right 5px
width 200px width 210px
height 25px height 25px
margin-bottom 6px margin 6px 0
.infoPanel-copyButton .infoPanel-copyButton
outline none outline none
@@ -214,3 +217,43 @@ body[data-theme="solarized-dark"]
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
&:hover &:hover
color $ui-solarized-ark-text-color 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

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl' import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n'
const InfoPanelTrashed = ({ const InfoPanelTrashed = ({
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
@@ -9,24 +10,24 @@ const InfoPanelTrashed = ({
<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'>MODIFICATION DATE</p> <p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
</div> </div>
<hr /> <hr />
<div> <div>
<p styleName='infoPanel-default'>{storageName}</p> <p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p> <p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
</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'>FOLDER</p> <p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'>{createdAt}</p> <p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p> <p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
</div> </div>
<div id='export-wrap'> <div id='export-wrap'>

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

@@ -19,6 +19,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import TrashButton from './TrashButton' import TrashButton from './TrashButton'
import FullscreenButton from './FullscreenButton' import FullscreenButton from './FullscreenButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton' import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton' import InfoButton from './InfoButton'
import ToggleModeButton from './ToggleModeButton' import ToggleModeButton from './ToggleModeButton'
@@ -27,6 +28,7 @@ import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter' import { formatDate } from 'browser/lib/date-formatter'
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags' import striptags from 'striptags'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
class MarkdownNoteDetail extends React.Component { class MarkdownNoteDetail extends React.Component {
constructor (props) { constructor (props) {
@@ -53,10 +55,14 @@ class MarkdownNoteDetail extends React.Component {
componentDidMount () { componentDidMount () {
ee.on('topbar:togglelockbutton', this.toggleLockButton) ee.on('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:togglemodebutton', () => {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType)
})
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) { if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
if (this.saveQueue != null) this.saveNow() if (this.saveQueue != null) this.saveNow()
this.setState({ this.setState({
note: Object.assign({}, nextProps.note) note: Object.assign({}, nextProps.note)
@@ -68,11 +74,8 @@ class MarkdownNoteDetail extends React.Component {
} }
componentWillUnmount () { componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
}
componentDidUnmount () {
ee.off('topbar:togglelockbutton', this.toggleLockButton) ee.off('topbar:togglelockbutton', this.toggleLockButton)
if (this.saveQueue != null) this.saveNow()
} }
handleUpdateTag () { handleUpdateTag () {
@@ -141,7 +144,7 @@ class MarkdownNoteDetail extends React.Component {
hashHistory.replace({ hashHistory.replace({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: newNote.storage + '-' + newNote.key key: newNote.key
} }
}) })
this.setState({ this.setState({
@@ -183,10 +186,10 @@ 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 const { confirmDeletion } = this.props.config.ui
if (isTrashed) { if (isTrashed) {
if (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)
@@ -198,11 +201,12 @@ class MarkdownNoteDetail extends React.Component {
noteKey: data.noteKey noteKey: data.noteKey
}) })
} }
ee.once('list:moved', dispatchHandler) ee.once('list:next', dispatchHandler)
}) })
.then(() => ee.emit('list:next'))
} }
} else { } else {
if (confirmDeletion()) { if (confirmDeleteNote(confirmDeletion, false)) {
note.isTrashed = true note.isTrashed = true
this.setState({ this.setState({
@@ -273,6 +277,7 @@ class MarkdownNoteDetail extends React.Component {
handleSwitchMode (type) { handleSwitchMode (type) {
this.setState({ editorType: type }, () => { this.setState({ editorType: type }, () => {
this.focus()
const newConfig = Object.assign({}, this.props.config) const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type newConfig.editor.type = type
ConfigManager.set(newConfig) ConfigManager.set(newConfig)
@@ -289,6 +294,7 @@ class MarkdownNoteDetail extends React.Component {
config={config} config={config}
value={note.content} value={note.content}
storageKey={note.storage} storageKey={note.storage}
noteKey={note.key}
onChange={this.handleUpdateContent.bind(this)} onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/> />
@@ -298,6 +304,7 @@ class MarkdownNoteDetail extends React.Component {
config={config} config={config}
value={note.content} value={note.content}
storageKey={note.storage} storageKey={note.storage}
noteKey={note.key}
onChange={this.handleUpdateContent.bind(this)} onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/> />
@@ -323,10 +330,7 @@ class MarkdownNoteDetail extends React.Component {
const trashTopBar = <div styleName='info'> const trashTopBar = <div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<i styleName='undo-button' <RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
className='fa fa-undo fa-fw'
onClick={(e) => this.handleUndoButtonClick(e)}
/>
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} /> <PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
@@ -361,16 +365,10 @@ class MarkdownNoteDetail extends React.Component {
value={this.state.note.tags} value={this.state.note.tags}
onChange={this.handleUpdateTag.bind(this)} onChange={this.handleUpdateTag.bind(this)}
/> />
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} /> <TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
<InfoButton <ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<StarButton <StarButton
onClick={(e) => this.handleStarButtonClick(e)} onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred} isActive={note.isStarred}
@@ -384,6 +382,7 @@ class MarkdownNoteDetail extends React.Component {
onMouseDown={(e) => this.handleLockButtonMouseDown(e)} onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
> >
<img styleName='iconInfo' src={imgSrc} /> <img styleName='iconInfo' src={imgSrc} />
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
</button> </button>
return ( return (
@@ -395,10 +394,14 @@ class MarkdownNoteDetail extends React.Component {
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} /> <TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanel <InfoPanel
storageName={currentOption.storage.name} storageName={currentOption.storage.name}
folderName={currentOption.folder.name} folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`} noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd} exportAsMd={this.exportAsMd}
@@ -442,8 +445,7 @@ MarkdownNoteDetail.propTypes = {
style: PropTypes.shape({ style: PropTypes.shape({
left: PropTypes.number left: PropTypes.number
}), }),
ignorePreviewPointerEvents: PropTypes.bool, ignorePreviewPointerEvents: PropTypes.bool
confirmDeletion: PropTypes.bool.isRequired
} }
export default CSSModules(MarkdownNoteDetail, styles) export default CSSModules(MarkdownNoteDetail, styles)

View File

@@ -7,16 +7,33 @@
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
box-shadow none box-shadow none
padding 20px 40px padding 20px 40px
overflow hidden
.lock-button .lock-button
padding-bottom 3px padding-bottom 3px
.control-lockButton .control-lockButton
top 150px
topBarButtonRight() topBarButtonRight()
position absolute
right 225px
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 35px
right -10px
width 50px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.trashed-infopanel .trashed-infopanel
top 40px
position relative position relative
.body .body
@@ -25,7 +42,7 @@
right 0 right 0
top $info-height + $info-margin-under-border top $info-height + $info-margin-under-border
bottom $statusBar-height bottom $statusBar-height
margin 0 45px margin 0 30px
.body-noteEditor .body-noteEditor
absolute top bottom left right absolute top bottom left right
@@ -54,3 +71,8 @@ body[data-theme="solarized-dark"]
.root .root
border-left 1px solid $ui-solarized-dark-borderColor border-left 1px solid $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color $ui-solarized-dark-noteDetail-backgroundColor
body[data-theme="monokai"]
.root
border-left 1px solid $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor

View File

@@ -1,6 +1,6 @@
@import('DetailVars') @import('DetailVars')
$info-height = 50px $info-height = 60px
$info-margin-under-border = 30px $info-margin-under-border = 30px
.info .info
@@ -8,11 +8,11 @@ $info-margin-under-border = 30px
left 0 left 0
right 0 right 0
height $info-height height $info-height
border-bottom 1px solid #eee
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
width 100% width 100%
display flex display flex
align-items center align-items center
padding 0 20px
.info-left .info-left
padding 0 10px padding 0 10px
@@ -20,7 +20,6 @@ $info-margin-under-border = 30px
display flex display flex
align-items center align-items center
.info-left-top-folderSelect .info-left-top-folderSelect
display flex display flex
align-items center align-items center
@@ -45,12 +44,9 @@ $info-margin-under-border = 30px
color $ui-button--color color $ui-button--color
.info-right .info-right
position absolute
right 40px
top 60px
bottom 1px
padding-left 30px
z-index 101 z-index 101
display inline-flex
margin-top 3px
.undo-button .undo-button
width 34px width 34px
@@ -102,3 +98,7 @@ body[data-theme="solarized-dark"]
border-color $ui-solarized-dark-borderColor border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color $ui-solarized-dark-noteDetail-backgroundColor
body[data-theme="monokai"]
.info
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl' import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const PermanentDeleteButton = ({ const PermanentDeleteButton = ({
onClick onClick
@@ -10,7 +11,7 @@ const PermanentDeleteButton = ({
onClick={(e) => onClick(e)} onClick={(e) => onClick(e)}
> >
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' /> <img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>Permanent Delete</span> <span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button> </button>
) )

View File

@@ -0,0 +1,22 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './RestoreButton.styl'
import i18n from 'browser/lib/i18n'
const RestoreButton = ({
onClick
}) => (
<button styleName='control-restoreButton'
onClick={onClick}
>
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
<span styleName='tooltip'>{i18n.__('Restore')}</span>
</button>
)
RestoreButton.propTypes = {
onClick: PropTypes.func.isRequired
}
export default CSSModules(RestoreButton, styles)

View File

@@ -0,0 +1,22 @@
.control-restoreButton
top 115px
topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
left 25px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="dark"]
.control-restoreButton
topBarButtonDark()

View File

@@ -8,7 +8,7 @@ import StarButton from './StarButton'
import TagSelect from './TagSelect' import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect' import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router' import {hashHistory} from 'react-router'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir' import 'codemirror-mode-elixir'
@@ -17,33 +17,22 @@ import StatusBar from '../StatusBar'
import context from 'browser/lib/context' import context from 'browser/lib/context'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import _ from 'lodash' import _ from 'lodash'
import { findNoteTitle } from 'browser/lib/findNoteTitle' import {findNoteTitle} from 'browser/lib/findNoteTitle'
import convertModeName from 'browser/lib/convertModeName'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import TrashButton from './TrashButton' import TrashButton from './TrashButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton' import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton' import InfoButton from './InfoButton'
import InfoPanel from './InfoPanel' import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed' import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter' import { formatDate } from 'browser/lib/date-formatter'
import i18n from 'browser/lib/i18n'
function pass (name) { import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
switch (name) {
case 'ejs':
return 'Embedded Javascript'
case 'html_ruby':
return 'Embedded Ruby'
case 'objectivec':
return 'Objective C'
case 'text':
return 'Plain Text'
default:
return name
}
}
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
class SnippetNoteDetail extends React.Component { class SnippetNoteDetail extends React.Component {
constructor (props) { constructor (props) {
@@ -52,16 +41,34 @@ class SnippetNoteDetail extends React.Component {
this.state = { this.state = {
isMovingNote: false, isMovingNote: false,
snippetIndex: 0, snippetIndex: 0,
showArrows: false,
enableLeftArrow: false,
enableRightArrow: false,
note: Object.assign({ note: Object.assign({
description: '' description: ''
}, props.note, { }, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet)) snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
}) })
} }
this.scrollToNextTabThreshold = 0.7
}
componentDidMount () {
const visibleTabs = this.visibleTabs
const allTabs = this.allTabs
if (visibleTabs.offsetWidth < allTabs.scrollWidth) {
this.setState({
showArrows: visibleTabs.offsetWidth < allTabs.scrollWidth,
enableRightArrow: allTabs.offsetLeft !== visibleTabs.offsetWidth - allTabs.scrollWidth,
enableLeftArrow: allTabs.offsetLeft !== 0
})
}
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) { if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
if (this.saveQueue != null) this.saveNow() if (this.saveQueue != null) this.saveNow()
const nextNote = Object.assign({ const nextNote = Object.assign({
description: '' description: ''
@@ -77,6 +84,7 @@ class SnippetNoteDetail extends React.Component {
this.refs['code-' + index].reload() this.refs['code-' + index].reload()
}) })
if (this.refs.tags) this.refs.tags.reset() if (this.refs.tags) this.refs.tags.reset()
this.setState(this.getArrowsState())
}) })
} }
} }
@@ -146,7 +154,7 @@ class SnippetNoteDetail extends React.Component {
hashHistory.replace({ hashHistory.replace({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: newNote.storage + '-' + newNote.key key: newNote.key
} }
}) })
this.setState({ this.setState({
@@ -176,10 +184,10 @@ class SnippetNoteDetail 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 const { confirmDeletion } = this.props.config.ui
if (isTrashed) { if (isTrashed) {
if (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)
@@ -191,11 +199,12 @@ class SnippetNoteDetail extends React.Component {
noteKey: data.noteKey noteKey: data.noteKey
}) })
} }
ee.once('list:moved', dispatchHandler) ee.once('list:next', dispatchHandler)
}) })
.then(() => ee.emit('list:next'))
} }
} else { } else {
if (confirmDeletion()) { if (confirmDeleteNote(confirmDeletion, false)) {
note.isTrashed = true note.isTrashed = true
this.setState({ this.setState({
@@ -226,6 +235,51 @@ class SnippetNoteDetail extends React.Component {
ee.emit('editor:fullscreen') ee.emit('editor:fullscreen')
} }
handleTabMoveLeftButtonClick (e) {
{
const left = this.visibleTabs.scrollLeft
const tabs = this.allTabs.querySelectorAll('div')
const lastVisibleTab = Array.from(tabs).find((tab) => {
return tab.offsetLeft + tab.offsetWidth >= left
})
if (lastVisibleTab) {
const visiblePart = lastVisibleTab.offsetWidth + lastVisibleTab.offsetLeft - left
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
const scrollToTab = (isFullyVisible && lastVisibleTab.previousSibling)
? lastVisibleTab.previousSibling
: lastVisibleTab
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
this.moveToTab(scrollToTab)
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'start', block: 'start'})
}
}
}
handleTabMoveRightButtonClick (e) {
const left = this.visibleTabs.scrollLeft
const width = this.visibleTabs.offsetWidth
const tabs = this.allTabs.querySelectorAll('div')
const lastVisibleTab = Array.from(tabs).find((tab) => {
return tab.offsetLeft + tab.offsetWidth >= width + left
})
if (lastVisibleTab) {
const visiblePart = width + left - lastVisibleTab.offsetLeft
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
const scrollToTab = (isFullyVisible && lastVisibleTab.nextSibling)
? lastVisibleTab.nextSibling
: lastVisibleTab
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
this.moveToTab(scrollToTab)
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'end', block: 'end'})
}
}
handleTabPlusButtonClick (e) { handleTabPlusButtonClick (e) {
this.addSnippet() this.addSnippet()
} }
@@ -262,9 +316,9 @@ class SnippetNoteDetail extends React.Component {
if (this.state.note.snippets[index].content.trim().length > 0) { if (this.state.note.snippets[index].content.trim().length > 0) {
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), { const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Delete a snippet', message: i18n.__('Delete a snippet'),
detail: 'This work cannot be undone.', detail: i18n.__('This work cannot be undone.'),
buttons: ['Confirm', 'Cancel'] buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}) })
if (dialogIndex === 0) { if (dialogIndex === 0) {
this.deleteSnippetByIndex(index) this.deleteSnippetByIndex(index)
@@ -285,6 +339,21 @@ class SnippetNoteDetail extends React.Component {
this.setState({ note, snippetIndex }, () => { this.setState({ note, snippetIndex }, () => {
this.save() this.save()
this.refs['code-' + this.state.snippetIndex].reload() this.refs['code-' + this.state.snippetIndex].reload()
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
console.log('no need for arrows')
this.moveTabBarBy(0)
} else {
const lastTab = this.allTabs.lastChild
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
console.log('need to scroll')
const width = this.visibleTabs.offsetWidth
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
} else {
this.setState(this.getArrowsState())
}
}
}) })
} }
@@ -299,11 +368,11 @@ class SnippetNoteDetail extends React.Component {
name: mode name: mode
}) })
} }
this.setState({note: Object.assign(this.state.note, {snippets: snippets})}) this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
this.setState({ this.setState(state => ({
note: this.state.note note: state.note
}, () => { }), () => {
this.save() this.save()
}) })
} }
@@ -312,11 +381,11 @@ class SnippetNoteDetail extends React.Component {
return (e) => { return (e) => {
const snippets = this.state.note.snippets.slice() const snippets = this.state.note.snippets.slice()
snippets[index].mode = name snippets[index].mode = name
this.setState({note: Object.assign(this.state.note, {snippets: snippets})}) this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
this.setState({ this.setState(state => ({
note: this.state.note note: state.note
}, () => { }), () => {
this.save() this.save()
}) })
@@ -330,10 +399,10 @@ class SnippetNoteDetail extends React.Component {
return (e) => { return (e) => {
const snippets = this.state.note.snippets.slice() const snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value snippets[index].content = this.refs['code-' + index].value
this.setState({note: Object.assign(this.state.note, {snippets: snippets})}) this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
this.setState({ this.setState(state => ({
note: this.state.note note: state.note
}, () => { }), () => {
this.save() this.save()
}) })
} }
@@ -341,6 +410,7 @@ class SnippetNoteDetail extends React.Component {
handleKeyDown (e) { handleKeyDown (e) {
switch (e.keyCode) { switch (e.keyCode) {
// tab key
case 9: case 9:
if (e.ctrlKey && !e.shiftKey) { if (e.ctrlKey && !e.shiftKey) {
e.preventDefault() e.preventDefault()
@@ -353,6 +423,7 @@ class SnippetNoteDetail extends React.Component {
this.focusEditor() this.focusEditor()
} }
break break
// L key
case 76: case 76:
{ {
const isSuper = global.process.platform === 'darwin' const isSuper = global.process.platform === 'darwin'
@@ -364,12 +435,13 @@ class SnippetNoteDetail extends React.Component {
} }
} }
break break
// T key
case 84: case 84:
{ {
const isSuper = global.process.platform === 'darwin' const isSuper = global.process.platform === 'darwin'
? e.metaKey ? e.metaKey
: e.ctrlKey : e.ctrlKey
if (isSuper) { if (isSuper && !e.shiftKey) {
e.preventDefault() e.preventDefault()
this.addSnippet() this.addSnippet()
} }
@@ -379,14 +451,14 @@ class SnippetNoteDetail extends React.Component {
} }
handleModeButtonClick (e, index) { handleModeButtonClick (e, index) {
const menu = new Menu() const templetes = []
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => { CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
menu.append(new MenuItem({ templetes.push({
label: mode.name, label: mode.name,
click: (e) => this.handleModeOptionClick(index, mode.name)(e) click: (e) => this.handleModeOptionClick(index, mode.name)(e)
})) })
}) })
menu.popup(remote.getCurrentWindow()) context.popup(templetes)
} }
handleIndentTypeButtonClick (e) { handleIndentTypeButtonClick (e) {
@@ -455,36 +527,88 @@ class SnippetNoteDetail extends React.Component {
this.refs.description.focus() this.refs.description.focus()
} }
moveToTab (tab) {
const easeOutCubic = t => (--t) * t * t + 1
const startScrollPosition = this.visibleTabs.scrollLeft
const animationTiming = 300
const scrollMoreCoeff = 1.4 // introduce coefficient, because we want to scroll a bit further to see next tab
let scrollBy = (tab.offsetLeft - startScrollPosition)
if (tab.offsetLeft > startScrollPosition) {
// if tab is on the right side and we want to show the whole tab in visible area,
// we need to include width of the tab and visible area in the formula
// ___________________________________________
// |____|_______|________|________|_show_this_|
// ↑_____________________↑
// visible area
scrollBy += (tab.offsetWidth - this.visibleTabs.offsetWidth)
}
let startTime = null
const scrollAnimation = time => {
startTime = startTime || time
const elapsed = (time - startTime) / animationTiming
this.visibleTabs.scrollLeft = startScrollPosition + easeOutCubic(elapsed) * scrollBy * scrollMoreCoeff
if (elapsed < 1) {
window.requestAnimationFrame(scrollAnimation)
} else {
this.setState(this.getArrowsState())
}
}
window.requestAnimationFrame(scrollAnimation)
}
getArrowsState () {
const allTabs = this.allTabs
const visibleTabs = this.visibleTabs
const showArrows = visibleTabs.offsetWidth < allTabs.scrollWidth
const enableRightArrow = visibleTabs.scrollLeft !== allTabs.scrollWidth - visibleTabs.offsetWidth
const enableLeftArrow = visibleTabs.scrollLeft !== 0
return {showArrows, enableRightArrow, enableLeftArrow}
}
addSnippet () { addSnippet () {
const { config } = this.props
const { note } = this.state const { note } = this.state
note.snippets = note.snippets.concat([{ note.snippets = note.snippets.concat([{
name: '', name: '',
mode: 'Plain Text', mode: config.editor.snippetDefaultLanguage || 'text',
content: '' content: ''
}]) }])
const snippetIndex = note.snippets.length - 1 const snippetIndex = note.snippets.length - 1
this.setState({ this.setState(Object.assign({
note, note,
snippetIndex snippetIndex
}, () => { }, this.getArrowsState()), () => {
if (this.state.showArrows) {
const tabs = this.allTabs.querySelectorAll('div')
if (tabs) {
this.moveToTab(tabs[snippetIndex])
}
}
this.refs['tab-' + snippetIndex].startRenaming() this.refs['tab-' + snippetIndex].startRenaming()
}) })
} }
jumpNextTab () { jumpNextTab () {
this.setState({ this.setState(state => ({
snippetIndex: (this.state.snippetIndex + 1) % this.state.note.snippets.length snippetIndex: (state.snippetIndex + 1) % state.note.snippets.length
}, () => { }), () => {
this.focusEditor() this.focusEditor()
}) })
} }
jumpPrevTab () { jumpPrevTab () {
this.setState({ this.setState(state => ({
snippetIndex: (this.state.snippetIndex - 1 + this.state.note.snippets.length) % this.state.note.snippets.length snippetIndex: (state.snippetIndex - 1 + state.note.snippets.length) % state.note.snippets.length
}, () => { }), () => {
this.focusEditor() this.focusEditor()
}) })
} }
@@ -502,9 +626,9 @@ class SnippetNoteDetail extends React.Component {
showWarning () { showWarning () {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Sorry!', message: i18n.__('Sorry!'),
detail: 'md/text import is available only a markdown note.', detail: i18n.__('md/text import is available only a markdown note.'),
buttons: ['OK'] buttons: [i18n.__('OK')]
}) })
} }
@@ -540,7 +664,7 @@ class SnippetNoteDetail extends React.Component {
const viewList = note.snippets.map((snippet, index) => { const viewList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index const isActive = this.state.snippetIndex === index
let syntax = CodeMirror.findModeByName(pass(snippet.mode)) let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
return <div styleName='tabView' return <div styleName='tabView'
@@ -564,8 +688,10 @@ class SnippetNoteDetail extends React.Component {
fontSize={editorFontSize} fontSize={editorFontSize}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorIndentSize} indentSize={editorIndentSize}
displayLineNumbers={config.editor.displayLineNumbers}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
onChange={(e) => this.handleCodeChange(index)(e)} onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index} ref={'code-' + index}
/> />
@@ -586,10 +712,7 @@ class SnippetNoteDetail extends React.Component {
const trashTopBar = <div styleName='info'> const trashTopBar = <div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<i styleName='undo-button' <RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
className='fa fa-undo fa-fw'
onClick={(e) => this.handleUndoButtonClick(e)}
/>
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} /> <PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
@@ -626,25 +749,27 @@ class SnippetNoteDetail extends React.Component {
/> />
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<StarButton <StarButton
onClick={(e) => this.handleStarButtonClick(e)} onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred} isActive={note.isStarred}
/> />
<button styleName='control-fullScreenButton' title='Fullscreen' <button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
onMouseDown={(e) => this.handleFullScreenButton(e)}> onMouseDown={(e) => this.handleFullScreenButton(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' /> <img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
</button> </button>
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} /> <TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanel <InfoPanel
storageName={currentOption.storage.name} storageName={currentOption.storage.name}
folderName={currentOption.folder.name} folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`} noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning} exportAsMd={this.showWarning}
@@ -670,16 +795,32 @@ class SnippetNoteDetail extends React.Component {
fontSize: parseInt(config.preview.fontSize, 10) fontSize: parseInt(config.preview.fontSize, 10)
}} }}
ref='description' ref='description'
placeholder='Description...' placeholder={i18n.__('Description...')}
value={this.state.note.description} value={this.state.note.description}
onChange={(e) => this.handleChange(e)} onChange={(e) => this.handleChange(e)}
/> />
</div> </div>
<div styleName='tabList'> <div styleName='tabList'>
<div styleName='list'> <button styleName='tabButton'
{tabList} hidden={!this.state.showArrows}
disabled={!this.state.enableLeftArrow}
onClick={(e) => this.handleTabMoveLeftButtonClick(e)}
>
<i className='fa fa-chevron-left' />
</button>
<div styleName='list' onScroll={(e) => { this.setState(this.getArrowsState()) }} ref={(tabs) => { this.visibleTabs = tabs }}>
<div styleName='allTabs' ref={(tabs) => { this.allTabs = tabs }}>
{tabList}
</div>
</div> </div>
<button styleName='plusButton' <button styleName='tabButton'
hidden={!this.state.showArrows}
disabled={!this.state.enableRightArrow}
onClick={(e) => this.handleTabMoveRightButtonClick(e)}
>
<i className='fa fa-chevron-right' />
</button>
<button styleName='tabButton'
onClick={(e) => this.handleTabPlusButtonClick(e)} onClick={(e) => this.handleTabPlusButtonClick(e)}
> >
<i className='fa fa-plus' /> <i className='fa fa-plus' />
@@ -693,7 +834,7 @@ class SnippetNoteDetail extends React.Component {
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)} onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
> >
{this.state.note.snippets[this.state.snippetIndex].mode == null {this.state.note.snippets[this.state.snippetIndex].mode == null
? 'Select Syntax...' ? i18n.__('Select Syntax...')
: this.state.note.snippets[this.state.snippetIndex].mode : this.state.note.snippets[this.state.snippetIndex].mode
}&nbsp; }&nbsp;
<i className='fa fa-caret-down' /> <i className='fa fa-caret-down' />
@@ -730,8 +871,7 @@ SnippetNoteDetail.propTypes = {
style: PropTypes.shape({ style: PropTypes.shape({
left: PropTypes.number left: PropTypes.number
}), }),
ignorePreviewPointerEvents: PropTypes.bool, ignorePreviewPointerEvents: PropTypes.bool
confirmDeletion: PropTypes.bool.isRequired
} }
export default CSSModules(SnippetNoteDetail, styles) export default CSSModules(SnippetNoteDetail, styles)

View File

@@ -9,8 +9,7 @@
.body .body
absolute left right absolute left right
left $snippet-note-detail-left-margin margin 0 30px
right $snippet-note-detail-right-margin
top $info-height + $info-margin-under-border top $info-height + $info-margin-under-border
bottom $statusBar-height bottom $statusBar-height
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
@@ -36,13 +35,26 @@
height 30px height 30px
display flex display flex
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
overflow hidden
.tabList .list .tabList .list
flex 1 flex 1
display flex
overflow hidden overflow hidden
overflow-x scroll
position relative
.tabList .plusButton &::-webkit-scrollbar {
display: none;
}
.allTabs
display flex
position relative
overflow visible
left 0
transition left 0.1s
.tabList .tabButton
navWhiteButtonColor() navWhiteButtonColor()
width 30px width 30px
@@ -70,6 +82,21 @@
top 80px top 80px
margin-bottom 10px margin-bottom 10px
topBarButtonRight() topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 70px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="white"] body[data-theme="white"]
.root .root
@@ -126,3 +153,20 @@ body[data-theme="solarized-dark"]
.tabList .tabList
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
body[data-theme="monokai"]
.root
border-left 1px solid $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
.body
background-color $ui-monokai-noteDetail-backgroundColor
.body .description textarea
background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color
border 1px solid $ui-monokai-borderColor
.tabList
background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './StarButton.styl' import styles from './StarButton.styl'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class StarButton extends React.Component { class StarButton extends React.Component {
constructor (props) { constructor (props) {
@@ -53,7 +54,7 @@ class StarButton extends React.Component {
: '../resources/icon/icon-star.svg' : '../resources/icon/icon-star.svg'
} }
/> />
<span styleName='tooltip'>Star</span> <span styleName='tooltip'>{i18n.__('Star')}</span>
</button> </button>
) )
} }

View File

@@ -11,9 +11,9 @@
tooltip() tooltip()
position absolute position absolute
pointer-events none pointer-events none
top 26px top 50px
right 0 right 115px
width 100% width 40px
z-index 200 z-index 200
padding 5px padding 5px
line-height normal line-height normal

View File

@@ -4,6 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl' import styles from './TagSelect.styl'
import _ from 'lodash' import _ from 'lodash'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter'
class TagSelect extends React.Component { class TagSelect extends React.Component {
constructor (props) { constructor (props) {
@@ -12,16 +14,26 @@ class TagSelect extends React.Component {
this.state = { this.state = {
newTag: '' newTag: ''
} }
this.addtagHandler = this.handleAddTag.bind(this)
} }
componentDidMount () { componentDidMount () {
this.value = this.props.value this.value = this.props.value
ee.on('editor:add-tag', this.addtagHandler)
} }
componentDidUpdate () { componentDidUpdate () {
this.value = this.props.value this.value = this.props.value
} }
componentWillUnmount () {
ee.off('editor:add-tag', this.addtagHandler)
}
handleAddTag () {
this.refs.newTag.focus()
}
handleNewTagInputKeyDown (e) { handleNewTagInputKeyDown (e) {
switch (e.keyCode) { switch (e.keyCode) {
case 9: case 9:
@@ -43,16 +55,9 @@ class TagSelect extends React.Component {
} }
removeLastTag () { removeLastTag () {
let { value } = this.props this.removeTagByCallback((value) => {
value.pop()
value = _.isArray(value) })
? value.slice()
: []
value.pop()
value = _.uniq(value)
this.value = value
this.props.onChange()
} }
reset () { reset () {
@@ -64,7 +69,8 @@ class TagSelect extends React.Component {
submitTag () { submitTag () {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
let { value } = this.props let { value } = this.props
const newTag = this.refs.newTag.value.trim().replace(/ +/g, '_') let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
if (newTag.length <= 0) { if (newTag.length <= 0) {
this.setState({ this.setState({
@@ -94,15 +100,22 @@ class TagSelect extends React.Component {
} }
handleTagRemoveButtonClick (tag) { handleTagRemoveButtonClick (tag) {
return (e) => { this.removeTagByCallback((value, tag) => {
let { value } = this.props
value.splice(value.indexOf(tag), 1) value.splice(value.indexOf(tag), 1)
value = _.uniq(value) }, tag)
}
this.value = value removeTagByCallback (callback, tag = null) {
this.props.onChange() let { value } = this.props
}
value = _.isArray(value)
? value.slice()
: []
callback(value, tag)
value = _.uniq(value)
this.value = value
this.props.onChange()
} }
render () { render () {
@@ -116,7 +129,7 @@ class TagSelect extends React.Component {
> >
<span styleName='tag-label'>#{tag}</span> <span styleName='tag-label'>#{tag}</span>
<button styleName='tag-removeButton' <button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)} onClick={(e) => this.handleTagRemoveButtonClick(tag)}
> >
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' /> <img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
</button> </button>
@@ -136,7 +149,7 @@ class TagSelect extends React.Component {
<input styleName='newTag' <input styleName='newTag'
ref='newTag' ref='newTag'
value={this.state.newTag} value={this.state.newTag}
placeholder='Add tag...' placeholder={i18n.__('Add tag...')}
onChange={(e) => this.handleNewTagInputChange(e)} onChange={(e) => this.handleNewTagInputChange(e)}
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)} onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
onBlur={(e) => this.handleNewTagBlur(e)} onBlur={(e) => this.handleNewTagBlur(e)}

View File

@@ -6,7 +6,8 @@
width 100% width 100%
overflow-x scroll overflow-x scroll
white-space nowrap white-space nowrap
margin-right 10px margin-top 31px
position absolute
.root::-webkit-scrollbar .root::-webkit-scrollbar
display none display none
@@ -81,3 +82,19 @@ body[data-theme="solarized-dark"]
border-color none border-color none
background-color transparent background-color transparent
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
body[data-theme="monokai"]
.tag
background-color $ui-monokai-button-backgroundColor
.tag-removeButton
border-color $ui-button--focus-borderColor
background-color transparent
.tag-label
color $ui-monokai-text-color
.newTag
border-color none
background-color transparent
color $ui-monokai-text-color

View File

@@ -2,18 +2,19 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl' import styles from './ToggleModeButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleModeButton = ({ const ToggleModeButton = ({
onClick, editorType onClick, editorType
}) => ( }) => (
<div styleName='control-toggleModeButton'> <div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}> <div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-split-on.svg' : '../resources/icon/icon-mode-split-on-active.svg'} /> <img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
</div> </div>
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}> <div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : '../resources/icon/icon-mode-markdown-off.svg'} /> <img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
</div> </div>
<span styleName='tooltip'>Toggle Mode</span> <span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
</div> </div>
) )

View File

@@ -1,24 +1,28 @@
.control-toggleModeButton .control-toggleModeButton
border 1px solid #eee height 25px
height 34px border-radius 50px
background-color #F4F4F4
width 52px
display flex display flex
align-items center align-items center
position: relative
top 2px
.active
background-color #1EC38B
width 33px
height 24px
box-shadow 2px 0px 7px #eee
z-index 1
div div
width 40px width 40px
height 100% height 100%
background-color #f9f9f9 border-radius 50%
display flex display flex
align-items center align-items center
justify-content center justify-content center
cursor pointer cursor pointer
&:first-child
border-right 1px solid #eee
.active
background-color #fff
box-shadow 2px 0px 7px #eee
z-index 1
&:hover .tooltip &:hover .tooltip
opacity 1 opacity 1
@@ -26,9 +30,10 @@
tooltip() tooltip()
position absolute position absolute
pointer-events none pointer-events none
top 47px top 33px
right 11px left -10px
z-index 200 z-index 200
width 80px
padding 5px padding 5px
line-height normal line-height normal
border-radius 2px border-radius 2px
@@ -40,22 +45,21 @@ body[data-theme="dark"]
topBarButtonDark() topBarButtonDark()
.control-toggleModeButton .control-toggleModeButton
border 1px solid #444444
div
background-color $ui-dark-noteDetail-backgroundColor
&:first-child
border-right 1px solid #444444
.active
background-color #3A404C background-color #3A404C
.active
background-color #1EC38B
box-shadow 2px 0px 7px #444444 box-shadow 2px 0px 7px #444444
body[data-theme="solarized-dark"] body[data-theme="solarized-dark"]
.control-toggleModeButton .control-toggleModeButton
border 1px solid #586E75
div
background-color $ui-solarized-dark-noteDetail-backgroundColor
&:first-child
border-right 1px solid #586E75
.active
background-color #002B36 background-color #002B36
.active
background-color #1EC38B
box-shadow 2px 0px 7px #222222
body[data-theme="monokai"]
.control-toggleModeButton
background-color #272822
.active
background-color #1EC38B
box-shadow 2px 0px 7px #222222 box-shadow 2px 0px 7px #222222

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl' import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const TrashButton = ({ const TrashButton = ({
onClick onClick
@@ -10,7 +11,7 @@ const TrashButton = ({
onClick={(e) => onClick(e)} onClick={(e) => onClick(e)}
> >
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' /> <img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>Trash</span> <span styleName='tooltip'>{i18n.__('Trash')}</span>
</button> </button>
) )

View File

@@ -8,8 +8,8 @@
tooltip() tooltip()
position absolute position absolute
pointer-events none pointer-events none
top 26px top 50px
right 0 right 50px
z-index 200 z-index 200
padding 5px padding 5px
line-height normal line-height normal

View File

@@ -7,6 +7,9 @@ import MarkdownNoteDetail from './MarkdownNoteDetail'
import SnippetNoteDetail from './SnippetNoteDetail' import SnippetNoteDetail from './SnippetNoteDetail'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import StatusBar from '../StatusBar' import StatusBar from '../StatusBar'
import i18n from 'browser/lib/i18n'
import debounceRender from 'react-debounce-render'
import searchFromNotes from 'browser/lib/search'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -32,35 +35,39 @@ class Detail extends React.Component {
ee.off('detail:delete', this.deleteHandler) ee.off('detail:delete', this.deleteHandler)
} }
confirmDeletion (permanent) { render () {
if (this.props.config.ui.confirmDeletion || permanent) { const { location, data, params, config } = this.props
const electron = require('electron') let note = null
const { remote } = electron
const { dialog } = remote
const alertConfig = { if (location.query.key != null) {
type: 'warning', const noteKey = location.query.key
message: 'Confirm note deletion', const allNotes = data.noteMap.map(note => note)
detail: 'This will permanently remove this note.', const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
buttons: ['Confirm', 'Cancel'] let displayedNotes = allNotes
if (location.pathname.match(/\/searched/)) {
const searchStr = params.searchword
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
: searchFromNotes(allNotes, searchStr)
} }
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), alertConfig) if (location.pathname.match(/\/tags/)) {
return dialogueButtonIndex === 0 const listOfTags = params.tagname.split(' ')
} displayedNotes = data.noteMap.map(note => note).filter(note =>
listOfTags.every(tag => note.tags.includes(tag))
)
}
return true if (location.pathname.match(/\/trashed/)) {
} displayedNotes = trashedNotes
} else {
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
}
render () { const noteKeys = displayedNotes.map(note => note.key)
const { location, data, config } = this.props if (noteKeys.includes(noteKey)) {
let note = null note = data.noteMap.get(noteKey)
if (location.query.key != null) { }
const splitted = location.query.key.split('-')
const storageKey = splitted.shift()
const noteKey = splitted.shift()
note = data.noteMap.get(storageKey + '-' + noteKey)
} }
if (note == null) { if (note == null) {
@@ -70,7 +77,7 @@ class Detail extends React.Component {
tabIndex='0' tabIndex='0'
> >
<div styleName='empty'> <div styleName='empty'>
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new note</div> <div styleName='empty-message'>{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />{i18n.__('to create a new note')}</div>
</div> </div>
<StatusBar <StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])} {..._.pick(this.props, ['config', 'location', 'dispatch'])}
@@ -84,7 +91,6 @@ class Detail extends React.Component {
<SnippetNoteDetail <SnippetNoteDetail
note={note} note={note}
config={config} config={config}
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
ref='root' ref='root'
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
@@ -101,7 +107,6 @@ class Detail extends React.Component {
<MarkdownNoteDetail <MarkdownNoteDetail
note={note} note={note}
config={config} config={config}
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
ref='root' ref='root'
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
@@ -123,4 +128,4 @@ Detail.propTypes = {
ignorePreviewPointerEvents: PropTypes.bool ignorePreviewPointerEvents: PropTypes.bool
} }
export default CSSModules(Detail, styles) export default debounceRender(CSSModules(Detail, styles))

View File

@@ -14,12 +14,14 @@ import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import { hashHistory } from 'react-router' import { hashHistory } from 'react-router'
import store from 'browser/main/store' import store from 'browser/main/store'
import i18n from 'browser/lib/i18n'
import { getLocales } from 'browser/lib/Languages'
import applyShortcuts from 'browser/main/lib/shortcutManager'
const path = require('path') const path = require('path')
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
class Main extends React.Component { class Main extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -57,10 +59,10 @@ class Main extends React.Component {
name: 'My Storage', name: 'My Storage',
path: path.join(remote.app.getPath('home'), 'Boostnote') path: path.join(remote.app.getPath('home'), 'Boostnote')
}) })
.then((data) => { .then(data => {
return data return data
}) })
.then((data) => { .then(data => {
if (data.storage.folders[0] != null) { if (data.storage.folders[0] != null) {
return data return data
} else { } else {
@@ -69,7 +71,7 @@ class Main extends React.Component {
color: '#1278BD', color: '#1278BD',
name: 'Default' name: 'Default'
}) })
.then((_data) => { .then(_data => {
return { return {
storage: _data.storage, storage: _data.storage,
notes: data.notes notes: data.notes
@@ -77,7 +79,7 @@ class Main extends React.Component {
}) })
} }
}) })
.then((data) => { .then(data => {
console.log(data) console.log(data)
store.dispatch({ store.dispatch({
type: 'ADD_STORAGE', type: 'ADD_STORAGE',
@@ -95,16 +97,16 @@ class Main extends React.Component {
{ {
name: 'example.html', name: 'example.html',
mode: 'html', mode: 'html',
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>' content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
}, },
{ {
name: 'example.js', name: 'example.js',
mode: 'javascript', mode: 'javascript',
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)' content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
} }
] ]
}) })
.then((note) => { .then(note => {
store.dispatch({ store.dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
@@ -117,7 +119,7 @@ class Main extends React.Component {
title: 'Welcome to Boostnote!', title: 'Welcome to Boostnote!',
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)' content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
}) })
.then((note) => { .then(note => {
store.dispatch({ store.dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
@@ -128,10 +130,10 @@ class Main extends React.Component {
.then(defaultMarkdownNote) .then(defaultMarkdownNote)
.then(() => data.storage) .then(() => data.storage)
}) })
.then((storage) => { .then(storage => {
hashHistory.push('/storages/' + storage.key) hashHistory.push('/storages/' + storage.key)
}) })
.catch((err) => { .catch(err => {
throw err throw err
}) })
} }
@@ -139,30 +141,33 @@ class Main extends React.Component {
componentDidMount () { componentDidMount () {
const { dispatch, config } = this.props const { dispatch, config } = this.props
if (config.ui.theme === 'dark') { const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai']
document.body.setAttribute('data-theme', 'dark')
} else if (config.ui.theme === 'white') { if (supportedThemes.indexOf(config.ui.theme) !== -1) {
document.body.setAttribute('data-theme', 'white') document.body.setAttribute('data-theme', config.ui.theme)
} else if (config.ui.theme === 'solarized-dark') {
document.body.setAttribute('data-theme', 'solarized-dark')
} else { } else {
document.body.setAttribute('data-theme', 'default') document.body.setAttribute('data-theme', 'default')
} }
if (getLocales().indexOf(config.ui.language) !== -1) {
i18n.setLocale(config.ui.language)
} else {
i18n.setLocale('en')
}
applyShortcuts()
// Reload all data // Reload all data
dataApi.init() dataApi.init().then(data => {
.then((data) => { dispatch({
dispatch({ type: 'INIT_ALL',
type: 'INIT_ALL', storages: data.storages,
storages: data.storages, notes: data.notes
notes: data.notes
})
if (data.storages.length < 1) {
this.init()
}
}) })
if (data.storages.length < 1) {
this.init()
}
})
eventEmitter.on('editor:fullscreen', this.toggleFullScreen) eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
} }
@@ -187,34 +192,40 @@ class Main extends React.Component {
handleMouseUp (e) { handleMouseUp (e) {
// Change width of NoteList component. // Change width of NoteList component.
if (this.state.isRightSliderFocused) { if (this.state.isRightSliderFocused) {
this.setState({ this.setState(
isRightSliderFocused: false {
}, () => { isRightSliderFocused: false
const { dispatch } = this.props },
const newListWidth = this.state.listWidth () => {
// TODO: ConfigManager should dispatch itself. const { dispatch } = this.props
ConfigManager.set({listWidth: newListWidth}) const newListWidth = this.state.listWidth
dispatch({ // TODO: ConfigManager should dispatch itself.
type: 'SET_LIST_WIDTH', ConfigManager.set({ listWidth: newListWidth })
listWidth: newListWidth dispatch({
}) type: 'SET_LIST_WIDTH',
}) listWidth: newListWidth
})
}
)
} }
// Change width of SideNav component. // Change width of SideNav component.
if (this.state.isLeftSliderFocused) { if (this.state.isLeftSliderFocused) {
this.setState({ this.setState(
isLeftSliderFocused: false {
}, () => { isLeftSliderFocused: false
const { dispatch } = this.props },
const navWidth = this.state.navWidth () => {
// TODO: ConfigManager should dispatch itself. const { dispatch } = this.props
ConfigManager.set({ navWidth }) const navWidth = this.state.navWidth
dispatch({ // TODO: ConfigManager should dispatch itself.
type: 'SET_NAV_WIDTH', ConfigManager.set({ navWidth })
navWidth dispatch({
}) type: 'SET_NAV_WIDTH',
}) navWidth
})
}
)
} }
} }
@@ -259,8 +270,8 @@ class Main extends React.Component {
} }
hideLeftLists (noteDetail, noteList, mainBody) { hideLeftLists (noteDetail, noteList, mainBody) {
this.setState({noteDetailWidth: noteDetail.style.left}) this.setState({ noteDetailWidth: noteDetail.style.left })
this.setState({mainBodyWidth: mainBody.style.left}) this.setState({ mainBodyWidth: mainBody.style.left })
noteDetail.style.left = '0px' noteDetail.style.left = '0px'
mainBody.style.left = '0px' mainBody.style.left = '0px'
noteList.style.display = 'none' noteList.style.display = 'none'
@@ -282,33 +293,36 @@ class Main extends React.Component {
<div <div
className='Main' className='Main'
styleName='root' styleName='root'
onMouseMove={(e) => this.handleMouseMove(e)} onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={(e) => this.handleMouseUp(e)} onMouseUp={e => this.handleMouseUp(e)}
> >
<SideNav <SideNav
{..._.pick(this.props, [ {..._.pick(this.props, ['dispatch', 'data', 'config', 'location'])}
'dispatch',
'data',
'config',
'location'
])}
width={this.state.navWidth} width={this.state.navWidth}
/> />
{!config.isSideNavFolded && {!config.isSideNavFolded &&
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'} <div
style={{left: this.state.navWidth}} styleName={
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)} this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
}
style={{ left: this.state.navWidth }}
onMouseDown={e => this.handleLeftSlideMouseDown(e)}
draggable='false' draggable='false'
> >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div> </div>}
} <div
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'} styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
id='main-body' id='main-body'
ref='body' ref='body'
style={{left: config.isSideNavFolded ? foldedNavigationWidth : this.state.navWidth}} style={{
left: config.isSideNavFolded
? foldedNavigationWidth
: this.state.navWidth
}}
> >
<TopBar style={{width: this.state.listWidth}} <TopBar
style={{ width: this.state.listWidth }}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'config', 'config',
@@ -317,7 +331,8 @@ class Main extends React.Component {
'location' 'location'
])} ])}
/> />
<NoteList style={{width: this.state.listWidth}} <NoteList
style={{ width: this.state.listWidth }}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'data', 'data',
@@ -326,15 +341,20 @@ class Main extends React.Component {
'location' 'location'
])} ])}
/> />
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'} <div
style={{left: this.state.listWidth - 1}} styleName={
onMouseDown={(e) => this.handleRightSlideMouseDown(e)} this.state.isRightSliderFocused
? 'slider-right--active'
: 'slider-right'
}
style={{ left: this.state.listWidth - 1 }}
onMouseDown={e => this.handleRightSlideMouseDown(e)}
draggable='false' draggable='false'
> >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div> </div>
<Detail <Detail
style={{left: this.state.listWidth}} style={{ left: this.state.listWidth }}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'data', 'data',
@@ -362,4 +382,4 @@ Main.propTypes = {
data: PropTypes.shape({}).isRequired data: PropTypes.shape({}).isRequired
} }
export default connect((x) => x)(CSSModules(Main, styles)) export default connect(x => x)(CSSModules(Main, styles))

View File

@@ -75,3 +75,7 @@ body[data-theme="dark"]
body[data-theme="solarized-dark"] body[data-theme="solarized-dark"]
.root, .root--expanded .root, .root--expanded
background-color $ui-solarized-dark-noteList-backgroundColor background-color $ui-solarized-dark-noteList-backgroundColor
body[data-theme="monokai"]
.root, .root--expanded
background-color $ui-monokai-noteList-backgroundColor

View File

@@ -6,6 +6,7 @@ import _ from 'lodash'
import modal from 'browser/main/lib/modal' import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal' import NewNoteModal from 'browser/main/modals/NewNoteModal'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron') const { remote } = require('electron')
const { dialog } = remote const { dialog } = remote
@@ -33,14 +34,15 @@ class NewNoteButton extends React.Component {
} }
handleNewNoteButtonClick (e) { handleNewNoteButtonClick (e) {
const { location, dispatch } = this.props const { location, dispatch, config } = this.props
const { storage, folder } = this.resolveTargetFolder() const { storage, folder } = this.resolveTargetFolder()
modal.open(NewNoteModal, { modal.open(NewNoteModal, {
storage: storage.key, storage: storage.key,
folder: folder.key, folder: folder.key,
dispatch, dispatch,
location location,
config
}) })
} }
@@ -56,9 +58,9 @@ class NewNoteButton extends React.Component {
} }
} }
if (storage == null) this.showMessageBox('No storage to create a note') if (storage == null) this.showMessageBox(i18n.__('No storage to create a note'))
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0] const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
if (folder == null) this.showMessageBox('No folder to create a note') if (folder == null) this.showMessageBox(i18n.__('No folder to create a note'))
return { return {
storage, storage,
@@ -86,7 +88,7 @@ class NewNoteButton extends React.Component {
onClick={(e) => this.handleNewNoteButtonClick(e)}> onClick={(e) => this.handleNewNoteButtonClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' /> <img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'> <span styleName='control-newNoteButton-tooltip'>
Make a note {OSX ? '⌘' : 'Ctrl'} + N {i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
</span> </span>
</button> </button>
</div> </div>

View File

@@ -114,3 +114,27 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
&:active &:active
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
body[data-theme="monokai"]
.root
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteList-backgroundColor
.control
background-color $ui-monokai-noteList-backgroundColor
border-color $ui-monokai-borderColor
.control-sortBy-select
&:hover
transition 0.2s
color $ui-monokai-text-color
.control-button
color $ui-monokai-inactive-text-color
&:hover
color $ui-monokai-text-color
.control-button--active
color $ui-monokai-text-color
&:active
color $ui-monokai-text-color

View File

@@ -1,11 +1,14 @@
/* global electron */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import debounceRender from 'react-debounce-render'
import styles from './NoteList.styl' import styles from './NoteList.styl'
import moment from 'moment' import moment from 'moment'
import _ from 'lodash' import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import NoteItem from 'browser/components/NoteItem' import NoteItem from 'browser/components/NoteItem'
import NoteItemSimple from 'browser/components/NoteItemSimple' import NoteItemSimple from 'browser/components/NoteItemSimple'
@@ -13,10 +16,16 @@ import searchFromNotes from 'browser/lib/search'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { hashHistory } from 'react-router' import { hashHistory } from 'react-router'
import copy from 'copy-to-clipboard'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import context from 'browser/lib/context'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
const WP_POST_PATH = '/wp/v2/posts'
function sortByCreatedAt (a, b) { function sortByCreatedAt (a, b) {
return new Date(b.createdAt) - new Date(a.createdAt) return new Date(b.createdAt) - new Date(a.createdAt)
@@ -31,7 +40,7 @@ function sortByUpdatedAt (a, b) {
} }
function findNoteByKey (notes, noteKey) { function findNoteByKey (notes, noteKey) {
return notes.find((note) => `${note.storage}-${note.key}` === noteKey) return notes.find((note) => note.key === noteKey)
} }
function findNotesByKeys (notes, noteKeys) { function findNotesByKeys (notes, noteKeys) {
@@ -39,7 +48,7 @@ function findNotesByKeys (notes, noteKeys) {
} }
function getNoteKey (note) { function getNoteKey (note) {
return `${note.storage}-${note.key}` return note.key
} }
class NoteList extends React.Component { class NoteList extends React.Component {
@@ -66,6 +75,11 @@ class NoteList extends React.Component {
this.deleteNote = this.deleteNote.bind(this) this.deleteNote = this.deleteNote.bind(this)
this.focusNote = this.focusNote.bind(this) this.focusNote = this.focusNote.bind(this)
this.pinToTop = this.pinToTop.bind(this) this.pinToTop = this.pinToTop.bind(this)
this.getNoteStorage = this.getNoteStorage.bind(this)
this.getNoteFolder = this.getNoteFolder.bind(this)
this.getViewType = this.getViewType.bind(this)
this.restoreNote = this.restoreNote.bind(this)
this.copyNoteLink = this.copyNoteLink.bind(this)
// TODO: not Selected noteKeys but SelectedNote(for reusing) // TODO: not Selected noteKeys but SelectedNote(for reusing)
this.state = { this.state = {
@@ -109,14 +123,27 @@ class NoteList extends React.Component {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
const { location } = this.props const { location } = this.props
const { selectedNoteKeys } = this.state
const visibleNoteKeys = this.notes.map(note => note.key)
const note = this.notes[0]
const prevKey = prevProps.location.query.key
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
if (this.notes.length > 0 && location.query.key == null) { if (note && location.query.key == null) {
const { router } = this.context const { router } = this.context
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes() if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
// A visible note is an active note
if (!selectedNoteKeys.includes(noteKey)) {
if (selectedNoteKeys.length === 1) selectedNoteKeys.pop()
selectedNoteKeys.push(noteKey)
ee.emit('list:moved')
}
router.replace({ router.replace({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: this.notes[0].storage + '-' + this.notes[0].key key: noteKey
} }
}) })
return return
@@ -169,9 +196,8 @@ class NoteList extends React.Component {
if (this.notes == null || this.notes.length === 0) { if (this.notes == null || this.notes.length === 0) {
return return
} }
let { router } = this.context let { selectedNoteKeys } = this.state
let { location } = this.props const { shiftKeyDown } = this.state
let { selectedNoteKeys, shiftKeyDown } = this.state
let targetIndex = this.getTargetIndex() let targetIndex = this.getTargetIndex()
@@ -197,9 +223,8 @@ class NoteList extends React.Component {
if (this.notes == null || this.notes.length === 0) { if (this.notes == null || this.notes.length === 0) {
return return
} }
let { router } = this.context let { selectedNoteKeys } = this.state
let { location } = this.props const { shiftKeyDown } = this.state
let { selectedNoteKeys, shiftKeyDown } = this.state
let targetIndex = this.getTargetIndex() let targetIndex = this.getTargetIndex()
const isTargetLastNote = targetIndex === this.notes.length - 1 const isTargetLastNote = targetIndex === this.notes.length - 1
@@ -240,30 +265,40 @@ class NoteList extends React.Component {
} }
handleNoteListKeyDown (e) { handleNoteListKeyDown (e) {
const { shiftKeyDown } = this.state
if (e.metaKey || e.ctrlKey) return true if (e.metaKey || e.ctrlKey) return true
// A key
if (e.keyCode === 65 && !e.shiftKey) { if (e.keyCode === 65 && !e.shiftKey) {
e.preventDefault() e.preventDefault()
ee.emit('top:new-note') ee.emit('top:new-note')
} }
// D key
if (e.keyCode === 68) { if (e.keyCode === 68) {
e.preventDefault() e.preventDefault()
this.deleteNote() this.deleteNote()
} }
// E key
if (e.keyCode === 69) { if (e.keyCode === 69) {
e.preventDefault() e.preventDefault()
ee.emit('detail:focus') ee.emit('detail:focus')
} }
if (e.keyCode === 38) { // L or S key
if (e.keyCode === 76 || e.keyCode === 83) {
e.preventDefault()
ee.emit('top:focus-search')
}
// UP or K key
if (e.keyCode === 38 || e.keyCode === 75) {
e.preventDefault() e.preventDefault()
this.selectPriorNote() this.selectPriorNote()
} }
if (e.keyCode === 40) { // DOWN or J key
if (e.keyCode === 40 || e.keyCode === 74) {
e.preventDefault() e.preventDefault()
this.selectNextNote() this.selectNextNote()
} }
@@ -282,7 +317,7 @@ class NoteList extends React.Component {
getNotes () { getNotes () {
const { data, params, location } = this.props const { data, params, location } = this.props
if (location.pathname.match(/\/home/) || location.pathname.match(/\alltags/)) { if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
const allNotes = data.noteMap.map((note) => note) const allNotes = data.noteMap.map((note) => note)
this.contextNotes = allNotes this.contextNotes = allNotes
return allNotes return allNotes
@@ -295,8 +330,10 @@ class NoteList extends React.Component {
} }
if (location.pathname.match(/\/searched/)) { if (location.pathname.match(/\/searched/)) {
const searchInputText = document.getElementsByClassName('searchInput')[0].value const searchInputText = params.searchword
if (searchInputText === '') { const allNotes = data.noteMap.map((note) => note)
this.contextNotes = allNotes
if (searchInputText === undefined || searchInputText === '') {
return this.sortByPin(this.contextNotes) return this.sortByPin(this.contextNotes)
} }
return searchFromNotes(this.contextNotes, searchInputText) return searchFromNotes(this.contextNotes, searchInputText)
@@ -309,11 +346,10 @@ class NoteList extends React.Component {
} }
if (location.pathname.match(/\/tags/)) { if (location.pathname.match(/\/tags/)) {
const listOfTags = params.tagname.split(' ')
return data.noteMap.map(note => { return data.noteMap.map(note => {
return note return note
}).filter(note => { }).filter(note => listOfTags.every(tag => note.tags.includes(tag)))
return note.tags.includes(params.tagname)
})
} }
return this.getContextNotes() return this.getContextNotes()
@@ -353,9 +389,10 @@ class NoteList extends React.Component {
} }
handleNoteClick (e, uniqueKey) { handleNoteClick (e, uniqueKey) {
let { router } = this.context const { router } = this.context
let { location } = this.props const { location } = this.props
let { shiftKeyDown, selectedNoteKeys } = this.state let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) { if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey) const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
@@ -381,10 +418,10 @@ class NoteList extends React.Component {
} }
handleSortByChange (e) { handleSortByChange (e) {
const { dispatch } = this.props const { dispatch, params: { folderKey } } = this.props
const config = { const config = {
sortBy: e.target.value [folderKey]: { sortBy: e.target.value }
} }
ConfigManager.set(config) ConfigManager.set(config)
@@ -413,20 +450,27 @@ class NoteList extends React.Component {
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') { if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Sorry!', message: i18n.__('Sorry!'),
detail: 'md/text import is available only a markdown note.', detail: i18n.__('md/text import is available only a markdown note.'),
buttons: ['OK', 'Cancel'] buttons: [i18n.__('OK'), i18n.__('Cancel')]
}) })
} }
} }
handleDragStart (e, note) { handleDragStart (e, note) {
const { selectedNoteKeys } = this.state let { selectedNoteKeys } = this.state
const noteKey = getNoteKey(note)
if (!selectedNoteKeys.includes(noteKey)) {
selectedNoteKeys = []
selectedNoteKeys.push(noteKey)
}
const notes = this.notes.map((note) => Object.assign({}, note)) const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys) const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const noteData = JSON.stringify(selectedNotes) const noteData = JSON.stringify(selectedNotes)
e.dataTransfer.setData('note', noteData) e.dataTransfer.setData('note', noteData)
this.setState({ selectedNoteKeys: [] }) this.selectNextNote()
} }
handleNoteContextMenu (e, uniqueKey) { handleNoteContextMenu (e, uniqueKey) {
@@ -439,45 +483,106 @@ class NoteList extends React.Component {
this.handleNoteClick(e, uniqueKey) this.handleNoteClick(e, uniqueKey)
} }
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top' const pinLabel = note.isPinned ? i18n.__('Remove pin') : i18n.__('Pin to Top')
const deleteLabel = 'Delete Note' const deleteLabel = i18n.__('Delete Note')
const cloneNote = i18n.__('Clone Note')
const restoreNote = i18n.__('Restore Note')
const copyNoteLink = i18n.__('Copy Note Link')
const publishLabel = i18n.__('Publish Blog')
const updateLabel = i18n.__('Update Blog')
const openBlogLabel = i18n.__('Open Blog')
const menu = new Menu() const templates = []
if (!location.pathname.match(/\/home|\/starred|\/trash/)) {
menu.append(new MenuItem({ if (location.pathname.match(/\/trash/)) {
label: pinLabel, templates.push({
click: this.pinToTop label: restoreNote,
})) click: this.restoreNote
}, {
label: deleteLabel,
click: this.deleteNote
})
} else {
if (!location.pathname.match(/\/starred/)) {
templates.push({
label: pinLabel,
click: this.pinToTop
})
}
templates.push({
label: deleteLabel,
click: this.deleteNote
}, {
label: cloneNote,
click: this.cloneNote.bind(this)
}, {
label: copyNoteLink,
click: this.copyNoteLink(note)
})
if (note.type === 'MARKDOWN_NOTE') {
if (note.blog && note.blog.blogLink && note.blog.blogId) {
templates.push({
label: updateLabel,
click: this.publishMarkdown.bind(this)
}, {
label: openBlogLabel,
click: () => this.openBlog.bind(this)(note)
})
} else {
templates.push({
label: publishLabel,
click: this.publishMarkdown.bind(this)
})
}
}
} }
menu.append(new MenuItem({ context.popup(templates)
label: deleteLabel,
click: this.deleteNote
}))
menu.popup()
} }
pinToTop () { updateSelectedNotes (updateFunc, cleanSelection = true) {
const { selectedNoteKeys } = this.state const { selectedNoteKeys } = this.state
const { dispatch } = this.props const { dispatch } = this.props
const notes = this.notes.map((note) => Object.assign({}, note)) const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys) const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
if (!_.isFunction(updateFunc)) {
console.warn('Update function is not defined. No update will happen')
updateFunc = (note) => { return note }
}
Promise.all( Promise.all(
selectedNotes.map((note) => { selectedNotes.map((note) => {
note.isPinned = !note.isPinned note = updateFunc(note)
return dataApi return dataApi
.updateNote(note.storage, note.key, note) .updateNote(note.storage, note.key, note)
})
)
.then((updatedNotes) => {
updatedNotes.forEach((note) => {
dispatch({
type: 'UPDATE_NOTE',
note
}) })
}) )
.then((updatedNotes) => {
updatedNotes.forEach((note) => {
dispatch({
type: 'UPDATE_NOTE',
note
})
})
})
if (cleanSelection) {
this.selectNextNote()
}
}
pinToTop () {
this.updateSelectedNotes((note) => {
note.isPinned = !note.isPinned
return note
})
}
restoreNote () {
this.updateSelectedNotes((note) => {
note.isTrashed = false
return note
}) })
this.setState({ selectedNoteKeys: [] })
} }
deleteNote () { deleteNote () {
@@ -486,22 +591,15 @@ class NoteList extends React.Component {
const notes = this.notes.map((note) => Object.assign({}, note)) const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys) const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const firstNote = selectedNotes[0] const firstNote = selectedNotes[0]
const { confirmDeletion } = this.props.config.ui
if (firstNote.isTrashed) { if (firstNote.isTrashed) {
const noteExp = selectedNotes.length > 1 ? 'notes' : 'note' if (!confirmDeleteNote(confirmDeletion, true)) return
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Confirm note deletion',
detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`,
buttons: ['Confirm', 'Cancel']
})
if (dialogueButtonIndex === 1) return
Promise.all( Promise.all(
selectedNoteKeys.map((uniqueKey) => { selectedNotes.map((note) => {
const storageKey = uniqueKey.split('-')[0]
const noteKey = uniqueKey.split('-')[1]
return dataApi return dataApi
.deleteNote(storageKey, noteKey) .deleteNote(note.storage, note.key)
}) })
) )
.then((data) => { .then((data) => {
@@ -518,6 +616,8 @@ class NoteList extends React.Component {
}) })
console.log('Notes were all deleted') console.log('Notes were all deleted')
} else { } else {
if (!confirmDeleteNote(confirmDeletion, false)) return
Promise.all( Promise.all(
selectedNotes.map((note) => { selectedNotes.map((note) => {
note.isTrashed = true note.isTrashed = true
@@ -543,6 +643,157 @@ class NoteList extends React.Component {
this.setState({ selectedNoteKeys: [] }) this.setState({ selectedNoteKeys: [] })
} }
cloneNote () {
const { selectedNoteKeys } = this.state
const { dispatch, location } = this.props
const { storage, folder } = this.resolveTargetFolder()
const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const firstNote = selectedNotes[0]
const eventName = firstNote.type === 'MARKDOWN_NOTE' ? 'ADD_MARKDOWN' : 'ADD_SNIPPET'
AwsMobileAnalyticsConfig.recordDynamicCustomEvent(eventName)
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
dataApi
.createNote(storage.key, {
type: firstNote.type,
folder: folder.key,
title: firstNote.title + ' ' + i18n.__('copy'),
content: firstNote.content
})
.then((note) => {
attachmentManagement.cloneAttachments(firstNote, note)
return note
})
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
this.setState({
selectedNoteKeys: [note.key]
})
hashHistory.push({
pathname: location.pathname,
query: {key: note.key}
})
})
}
copyNoteLink (note) {
const noteLink = `[${note.title}](:note:${note.key})`
return copy(noteLink)
}
save (note) {
const { dispatch } = this.props
dataApi
.updateNote(note.storage, note.key, note)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
}
publishMarkdown () {
if (this.pendingPublish) {
clearTimeout(this.pendingPublish)
}
this.pendingPublish = setTimeout(() => {
this.publishMarkdownNow()
}, 1000)
}
publishMarkdownNow () {
const {selectedNoteKeys} = this.state
const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const firstNote = selectedNotes[0]
const config = ConfigManager.get()
const {address, token, authMethod, username, password} = config.blog
let authToken = ''
if (authMethod === 'USER') {
authToken = `Basic ${window.btoa(`${username}:${password}`)}`
} else {
authToken = `Bearer ${token}`
}
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
const markdown = new Markdown()
const data = {
title: firstNote.title,
content: markdown.render(contentToRender),
status: 'publish'
}
let url = ''
let method = ''
if (firstNote.blog && firstNote.blog.blogId) {
url = `${address}${WP_POST_PATH}/${firstNote.blog.blogId}`
method = 'PUT'
} else {
url = `${address}${WP_POST_PATH}`
method = 'POST'
}
// eslint-disable-next-line no-undef
fetch(url, {
method: method,
body: JSON.stringify(data),
headers: {
'Authorization': authToken,
'Content-Type': 'application/json'
}
}).then(res => res.json())
.then(response => {
if (_.isNil(response.link) || _.isNil(response.id)) {
return Promise.reject()
}
firstNote.blog = {
blogLink: response.link,
blogId: response.id
}
this.save(firstNote)
this.confirmPublish(firstNote)
})
.catch((error) => {
console.error(error)
this.confirmPublishError()
})
}
confirmPublishError () {
const { remote } = electron
const { dialog } = remote
const alertError = {
type: 'warning',
message: i18n.__('Publish Failed'),
detail: i18n.__('Check and update your blog setting and try again.'),
buttons: [i18n.__('Confirm')]
}
dialog.showMessageBox(remote.getCurrentWindow(), alertError)
}
confirmPublish (note) {
const buttonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Publish Succeeded'),
detail: `${note.title} is published at ${note.blog.blogLink}`,
buttons: [i18n.__('Confirm'), i18n.__('Open Blog')]
})
if (buttonIndex === 1) {
this.openBlog(note)
}
}
openBlog (note) {
const { shell } = electron
shell.openExternal(note.blog.blogLink)
}
importFromFile () { importFromFile () {
const options = { const options = {
filters: [ filters: [
@@ -635,19 +886,39 @@ class NoteList extends React.Component {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: message, message: message,
buttons: ['OK'] buttons: [i18n.__('OK')]
}) })
} }
getNoteStorage (note) { // note.storage = storage key
return this.props.data.storageMap.toJS()[note.storage]
}
getNoteFolder (note) { // note.folder = folder key
return _.find(this.getNoteStorage(note).folders, ({ key }) => key === note.folder)
}
getViewType () {
const { pathname } = this.props.location
const folder = /\/folders\/[a-zA-Z0-9]+/.test(pathname)
const storage = /\/storages\/[a-zA-Z0-9]+/.test(pathname) && !folder
const allNotes = pathname === '/home'
if (allNotes) return 'ALL'
if (folder) return 'FOLDER'
if (storage) return 'STORAGE'
}
render () { render () {
let { location, notes, config, dispatch } = this.props const { location, config, params: { folderKey } } = this.props
let { selectedNoteKeys } = this.state let { notes } = this.props
let sortFunc = config.sortBy === 'CREATED_AT' const { selectedNoteKeys } = this.state
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
const sortFunc = sortBy === 'CREATED_AT'
? sortByCreatedAt ? sortByCreatedAt
: config.sortBy === 'ALPHABETICAL' : sortBy === 'ALPHABETICAL'
? sortByAlphabetical ? sortByAlphabetical
: sortByUpdatedAt : sortByUpdatedAt
const sortedNotes = location.pathname.match(/\/home|\/starred|\/trash/) const sortedNotes = location.pathname.match(/\/starred|\/trash/)
? this.getNotes().sort(sortFunc) ? this.getNotes().sort(sortFunc)
: this.sortByPin(this.getNotes().sort(sortFunc)) : this.sortByPin(this.getNotes().sort(sortFunc))
this.notes = notes = sortedNotes.filter((note) => { this.notes = notes = sortedNotes.filter((note) => {
@@ -655,7 +926,7 @@ class NoteList extends React.Component {
if (note.isTrashed !== true || location.pathname === '/trashed') return true if (note.isTrashed !== true || location.pathname === '/trashed') return true
}) })
moment.locale('en', { moment.updateLocale('en', {
relativeTime: { relativeTime: {
future: 'in %s', future: 'in %s',
past: '%s ago', past: '%s ago',
@@ -674,20 +945,30 @@ class NoteList extends React.Component {
} }
}) })
const viewType = this.getViewType()
const autoSelectFirst =
notes.length === 1 ||
selectedNoteKeys.length === 0 ||
notes.every(note => !selectedNoteKeys.includes(note.key))
const noteList = notes const noteList = notes
.map(note => { .map((note, index) => {
if (note == null) { if (note == null) {
return null return null
} }
const isDefault = config.listStyle === 'DEFAULT' const isDefault = config.listStyle === 'DEFAULT'
const uniqueKey = getNoteKey(note) const uniqueKey = getNoteKey(note)
const isActive = selectedNoteKeys.includes(uniqueKey)
const isActive =
selectedNoteKeys.includes(uniqueKey) ||
notes.length === 1 ||
(autoSelectFirst && index === 0)
const dateDisplay = moment( const dateDisplay = moment(
config.sortBy === 'CREATED_AT' sortBy === 'CREATED_AT'
? note.createdAt : note.updatedAt ? note.createdAt : note.updatedAt
).fromNow('D') ).fromNow('D')
const key = `${note.storage}-${note.key}`
if (isDefault) { if (isDefault) {
return ( return (
@@ -700,6 +981,9 @@ class NoteList extends React.Component {
handleNoteClick={this.handleNoteClick.bind(this)} handleNoteClick={this.handleNoteClick.bind(this)}
handleDragStart={this.handleDragStart.bind(this)} handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname} pathname={location.pathname}
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
/> />
) )
} }
@@ -713,6 +997,9 @@ class NoteList extends React.Component {
handleNoteClick={this.handleNoteClick.bind(this)} handleNoteClick={this.handleNoteClick.bind(this)}
handleDragStart={this.handleDragStart.bind(this)} handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname} pathname={location.pathname}
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
/> />
) )
}) })
@@ -727,16 +1014,17 @@ class NoteList extends React.Component {
<div styleName='control-sortBy'> <div styleName='control-sortBy'>
<i className='fa fa-angle-down' /> <i className='fa fa-angle-down' />
<select styleName='control-sortBy-select' <select styleName='control-sortBy-select'
value={config.sortBy} title={i18n.__('Select filter mode')}
value={sortBy}
onChange={(e) => this.handleSortByChange(e)} onChange={(e) => this.handleSortByChange(e)}
> >
<option value='UPDATED_AT'>Updated</option> <option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
<option value='CREATED_AT'>Created</option> <option title='Sort by create time' value='CREATED_AT'>{i18n.__('Created')}</option>
<option value='ALPHABETICAL'>Alphabetically</option> <option title='Sort alphabetically' value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
</select> </select>
</div> </div>
<div styleName='control-button-area'> <div styleName='control-button-area'>
<button styleName={config.listStyle === 'DEFAULT' <button title={i18n.__('Default View')} styleName={config.listStyle === 'DEFAULT'
? 'control-button--active' ? 'control-button--active'
: 'control-button' : 'control-button'
} }
@@ -744,7 +1032,7 @@ class NoteList extends React.Component {
> >
<img styleName='iconTag' src='../resources/icon/icon-column.svg' /> <img styleName='iconTag' src='../resources/icon/icon-column.svg' />
</button> </button>
<button styleName={config.listStyle === 'SMALL' <button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
? 'control-button--active' ? 'control-button--active'
: 'control-button' : 'control-button'
} }
@@ -778,4 +1066,4 @@ NoteList.propTypes = {
}) })
} }
export default CSSModules(NoteList, styles) export default debounceRender(CSSModules(NoteList, styles))

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl' import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const ListButton = ({ const ListButton = ({
onClick, isTagActive onClick, isTagActive
@@ -12,7 +13,7 @@ const ListButton = ({
: '../resources/icon/icon-list-active.svg' : '../resources/icon/icon-list-active.svg'
} }
/> />
<span styleName='tooltip'>Notes</span> <span styleName='tooltip'>{i18n.__('Notes')}</span>
</button> </button>
) )

View File

@@ -2,13 +2,14 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferenceButton.styl' import styles from './PreferenceButton.styl'
import i18n from 'browser/lib/i18n'
const PreferenceButton = ({ const PreferenceButton = ({
onClick onClick
}) => ( }) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}> <button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' /> <img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>Preferences</span> <span styleName='tooltip'>{i18n.__('Preferences')}</span>
</button> </button>
) )

View File

@@ -49,3 +49,4 @@ body[data-theme="dark"]
border-radius 2px border-radius 2px
opacity 0 opacity 0
transition 0.1s transition 0.1s
white-space nowrap

View File

@@ -30,11 +30,33 @@
display flex display flex
flex-direction column flex-direction column
.tag-title .tag-control
padding-left 15px display flex
padding-bottom 13px height 30px
p line-height 25px
color $ui-button-default-color overflow hidden
.tag-control-title
padding-left 15px
padding-bottom 13px
flex 1
p
color $ui-button-default-color
.tag-control-sortTagsBy
user-select none
font-size 12px
color $ui-inactive-text-color
margin-left 12px
margin-right 12px
.tag-control-sortTagsBy-select
appearance: none;
margin-left 5px
color $ui-inactive-text-color
padding 0
border none
background-color transparent
outline none
cursor pointer
font-size 12px
.tagList .tagList
overflow-y auto overflow-y auto
@@ -95,3 +117,8 @@ body[data-theme="solarized-dark"]
.root, .root--folded .root, .root--folded
background-color $ui-solarized-dark-backgroundColor background-color $ui-solarized-dark-backgroundColor
border-right 1px solid $ui-solarized-dark-borderColor border-right 1px solid $ui-solarized-dark-borderColor
body[data-theme="monokai"]
.root, .root--folded
background-color $ui-monokai-backgroundColor
border-right 1px solid $ui-monokai-borderColor

View File

@@ -8,46 +8,49 @@ import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
import RenameFolderModal from 'browser/main/modals/RenameFolderModal' import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import StorageItemChild from 'browser/components/StorageItem' import StorageItemChild from 'browser/components/StorageItem'
import eventEmitter from 'browser/main/lib/eventEmitter'
import _ from 'lodash' import _ from 'lodash'
import * as path from 'path' import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
const escapeStringRegexp = require('escape-string-regexp')
const path = require('path')
class StorageItem extends React.Component { class StorageItem extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
const { storage } = this.props
this.state = { this.state = {
isOpen: true isOpen: !!storage.isOpen
} }
} }
handleHeaderContextMenu (e) { handleHeaderContextMenu (e) {
const menu = Menu.buildFromTemplate([ context.popup([
{ {
label: 'Add Folder', label: i18n.__('Add Folder'),
click: (e) => this.handleAddFolderButtonClick(e) click: (e) => this.handleAddFolderButtonClick(e)
}, },
{ {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Unlink Storage', label: i18n.__('Unlink Storage'),
click: (e) => this.handleUnlinkStorageClick(e) click: (e) => this.handleUnlinkStorageClick(e)
} }
]) ])
menu.popup()
} }
handleUnlinkStorageClick (e) { handleUnlinkStorageClick (e) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), { const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Unlink Storage', message: i18n.__('Unlink Storage'),
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)', detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'),
buttons: ['Confirm', 'Cancel'] buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}) })
if (index === 0) { if (index === 0) {
@@ -66,8 +69,18 @@ class StorageItem extends React.Component {
} }
handleToggleButtonClick (e) { handleToggleButtonClick (e) {
const { storage, dispatch } = this.props
const isOpen = !this.state.isOpen
dataApi.toggleStorage(storage.key, isOpen)
.then((storage) => {
dispatch({
type: 'EXPAND_STORAGE',
storage,
isOpen
})
})
this.setState({ this.setState({
isOpen: !this.state.isOpen isOpen: isOpen
}) })
} }
@@ -92,23 +105,23 @@ class StorageItem extends React.Component {
} }
handleFolderButtonContextMenu (e, folder) { handleFolderButtonContextMenu (e, folder) {
const menu = Menu.buildFromTemplate([ context.popup([
{ {
label: 'Rename Folder', label: i18n.__('Rename Folder'),
click: (e) => this.handleRenameFolderClick(e, folder) click: (e) => this.handleRenameFolderClick(e, folder)
}, },
{ {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Export Folder', label: i18n.__('Export Folder'),
submenu: [ submenu: [
{ {
label: 'Export as txt', label: i18n.__('Export as txt'),
click: (e) => this.handleExportFolderClick(e, folder, 'txt') click: (e) => this.handleExportFolderClick(e, folder, 'txt')
}, },
{ {
label: 'Export as md', label: i18n.__('Export as md'),
click: (e) => this.handleExportFolderClick(e, folder, 'md') click: (e) => this.handleExportFolderClick(e, folder, 'md')
} }
] ]
@@ -117,12 +130,10 @@ class StorageItem extends React.Component {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Delete Folder', label: i18n.__('Delete Folder'),
click: (e) => this.handleFolderDeleteClick(e, folder) click: (e) => this.handleFolderDeleteClick(e, folder)
} }
]) ])
menu.popup()
} }
handleRenameFolderClick (e, folder) { handleRenameFolderClick (e, folder) {
@@ -136,8 +147,8 @@ class StorageItem extends React.Component {
handleExportFolderClick (e, folder, fileType) { handleExportFolderClick (e, folder, fileType) {
const options = { const options = {
properties: ['openDirectory', 'createDirectory'], properties: ['openDirectory', 'createDirectory'],
buttonLabel: 'Select directory', buttonLabel: i18n.__('Select directory'),
title: 'Select a folder to export the files to', title: i18n.__('Select a folder to export the files to'),
multiSelections: false multiSelections: false
} }
dialog.showOpenDialog(remote.getCurrentWindow(), options, dialog.showOpenDialog(remote.getCurrentWindow(), options,
@@ -161,9 +172,9 @@ class StorageItem extends React.Component {
handleFolderDeleteClick (e, folder) { handleFolderDeleteClick (e, folder) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), { const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Delete Folder', message: i18n.__('Delete Folder'),
detail: 'This will delete all notes in the folder and can not be undone.', detail: i18n.__('This will delete all notes in the folder and can not be undone.'),
buttons: ['Confirm', 'Cancel'] buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}) })
if (index === 0) { if (index === 0) {
@@ -193,33 +204,16 @@ class StorageItem extends React.Component {
dropNote (storage, folder, dispatch, location, noteData) { dropNote (storage, folder, dispatch, location, noteData) {
noteData = noteData.filter((note) => folder.key !== note.folder) noteData = noteData.filter((note) => folder.key !== note.folder)
if (noteData.length === 0) return if (noteData.length === 0) return
const newNoteData = noteData.map((note) => Object.assign({}, note, {storage: storage, folder: folder.key}))
Promise.all( Promise.all(
newNoteData.map((note) => dataApi.createNote(storage.key, note)) noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key))
) )
.then((createdNoteData) => { .then((createdNoteData) => {
createdNoteData.forEach((note) => { createdNoteData.forEach((newNote) => {
dispatch({ dispatch({
type: 'UPDATE_NOTE', type: 'MOVE_NOTE',
note: note originNote: noteData.find((note) => note.content === newNote.oldContent),
}) note: newNote
})
})
.catch((err) => {
console.error(`error on create notes: ${err}`)
})
.then(() => {
return Promise.all(
noteData.map((note) => dataApi.deleteNote(note.storage, note.key))
)
})
.then((deletedNoteData) => {
deletedNoteData.forEach((note) => {
dispatch({
type: 'DELETE_NOTE',
storageKey: note.storageKey,
noteKey: note.noteKey
}) })
}) })
}) })
@@ -238,8 +232,10 @@ class StorageItem extends React.Component {
render () { render () {
const { storage, location, isFolded, data, dispatch } = this.props const { storage, location, isFolded, data, dispatch } = this.props
const { folderNoteMap, trashedSet } = data const { folderNoteMap, trashedSet } = data
const folderList = storage.folders.map((folder) => { const SortableStorageItemChild = SortableElement(StorageItemChild)
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key))) const folderList = storage.folders.map((folder, index) => {
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
const isActive = !!(location.pathname.match(folderRegex))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key) const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
let noteCount = 0 let noteCount = 0
@@ -252,8 +248,9 @@ class StorageItem extends React.Component {
noteCount = noteSet.size - trashedNoteCount noteCount = noteSet.size - trashedNoteCount
} }
return ( return (
<StorageItemChild <SortableStorageItemChild
key={folder.key} key={folder.key}
index={index}
isActive={isActive} isActive={isActive}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)} handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)} handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
@@ -268,16 +265,16 @@ class StorageItem extends React.Component {
) )
}) })
const isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$')) const isActive = location.pathname.match(new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + '$'))
return ( return (
<div styleName={isFolded ? 'root--folded' : 'root'} <div styleName={isFolded ? 'root--folded' : 'root'}
key={storage.key} key={storage.key}
> >
<div styleName={isActive <div styleName={isActive
? 'header--active' ? 'header--active'
: 'header' : 'header'
} }
onContextMenu={(e) => this.handleHeaderContextMenu(e)} onContextMenu={(e) => this.handleHeaderContextMenu(e)}
> >
<button styleName='header-toggleButton' <button styleName='header-toggleButton'
@@ -286,7 +283,7 @@ class StorageItem extends React.Component {
<img src={this.state.isOpen <img src={this.state.isOpen
? '../resources/icon/icon-down.svg' ? '../resources/icon/icon-down.svg'
: '../resources/icon/icon-right.svg' : '../resources/icon/icon-right.svg'
} }
/> />
</button> </button>

View File

@@ -44,7 +44,7 @@
height 36px height 36px
padding-left 25px padding-left 25px
padding-right 15px padding-right 15px
line-height 22px line-height 36px
cursor pointer cursor pointer
font-size 14px font-size 14px
border none border none

View File

@@ -29,6 +29,7 @@
border-radius 2px border-radius 2px
opacity 0 opacity 0
transition 0.1s transition 0.1s
white-space nowrap
body[data-theme="white"] body[data-theme="white"]
.non-active-button .non-active-button

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl' import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const TagButton = ({ const TagButton = ({
onClick, isTagActive onClick, isTagActive
@@ -12,7 +13,7 @@ const TagButton = ({
: '../resources/icon/icon-tag.svg' : '../resources/icon/icon-tag.svg'
} }
/> />
<span styleName='tooltip'>Tags</span> <span styleName='tooltip'>{i18n.__('Tags')}</span>
</button> </button>
) )

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import styles from './SideNav.styl' import styles from './SideNav.styl'
import { openModal } from 'browser/main/lib/modal' import { openModal } from 'browser/main/lib/modal'
import PreferencesModal from '../modals/PreferencesModal' import PreferencesModal from '../modals/PreferencesModal'
@@ -14,6 +15,9 @@ import EventEmitter from 'browser/main/lib/eventEmitter'
import PreferenceButton from './PreferenceButton' import PreferenceButton from './PreferenceButton'
import ListButton from './ListButton' import ListButton from './ListButton'
import TagButton from './TagButton' import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
class SideNav extends React.Component { class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7 // TODO: should not use electron stuff v0.7
@@ -65,8 +69,19 @@ class SideNav extends React.Component {
router.push('/alltags') router.push('/alltags')
} }
onSortEnd (storage) {
return ({oldIndex, newIndex}) => {
const { dispatch } = this.props
dataApi
.reorderFolder(storage.key, oldIndex, newIndex)
.then((data) => {
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
})
}
}
SideNavComponent (isFolded, storageList) { SideNavComponent (isFolded, storageList) {
const { location, data } = this.props const { location, data, config } = this.props
const isHomeActive = !!location.pathname.match(/^\/home$/) const isHomeActive = !!location.pathname.match(/^\/home$/)
const isStarredActive = !!location.pathname.match(/^\/starred$/) const isStarredActive = !!location.pathname.match(/^\/starred$/)
@@ -86,20 +101,36 @@ class SideNav extends React.Component {
isTrashedActive={isTrashedActive} isTrashedActive={isTrashedActive}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)} handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)} handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
counterTotalNote={data.noteMap._map.size} counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
counterStarredNote={data.starredSet._set.size} counterStarredNote={data.starredSet._set.size}
counterDelNote={data.trashedSet._set.size} counterDelNote={data.trashedSet._set.size}
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
/> />
<StorageList storageList={storageList} /> <StorageList storageList={storageList} isFolded={isFolded} />
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} /> <NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
</div> </div>
) )
} else { } else {
component = ( component = (
<div styleName='tabBody'> <div styleName='tabBody'>
<div styleName='tag-title'> <div styleName='tag-control'>
<p>Tags</p> <div styleName='tag-control-title'>
<p>{i18n.__('Tags')}</p>
</div>
<div styleName='tag-control-sortTagsBy'>
<i className='fa fa-angle-down' />
<select styleName='tag-control-sortTagsBy-select'
title={i18n.__('Select filter mode')}
value={config.sortTagsBy}
onChange={(e) => this.handleSortTagsByChange(e)}
>
<option title='Sort alphabetically'
value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
<option title='Sort by update time'
value='COUNTER'>{i18n.__('Counter')}</option>
</select>
</div>
</div> </div>
<div styleName='tagList'> <div styleName='tagList'>
{this.tagListComponent(data)} {this.tagListComponent(data)}
@@ -112,26 +143,62 @@ class SideNav extends React.Component {
} }
tagListComponent () { tagListComponent () {
const { data, location } = this.props const { data, location, config } = this.props
const tagList = data.tagNoteMap.map((tag, key) => { const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
return key let tagList = _.sortBy(data.tagNoteMap.map(
}) (tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
), ['name']).filter(
tag => tag.size > 0
)
if (config.sortTagsBy === 'COUNTER') {
tagList = _.sortBy(tagList, item => (0 - item.size))
}
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
tagList = tagList.filter(
tag => tag.related
)
}
return ( return (
tagList.map(tag => ( tagList.map(tag => {
<TagListItem return (
name={tag} <TagListItem
handleClickTagListItem={this.handleClickTagListItem.bind(this)} name={tag.name}
isActive={this.getTagActive(location.pathname, tag)} handleClickTagListItem={this.handleClickTagListItem.bind(this)}
key={tag} handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
/> isActive={this.getTagActive(location.pathname, tag.name)}
)) isRelated={tag.related}
key={tag.name}
count={tag.size}
/>
)
})
) )
} }
getRelatedTags (activeTags, noteMap) {
if (activeTags.length === 0) {
return new Set()
}
const relatedNotes = noteMap.map(
note => ({key: note.key, tags: note.tags})
).filter(
note => activeTags.every(tag => note.tags.includes(tag))
)
const relatedTags = new Set()
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
return relatedTags
}
getTagActive (path, tag) { getTagActive (path, tag) {
return this.getActiveTags(path).includes(tag)
}
getActiveTags (path) {
const pathSegments = path.split('/') const pathSegments = path.split('/')
const pathTag = pathSegments[pathSegments.length - 1] const tags = pathSegments[pathSegments.length - 1]
return pathTag === tag return (tags === 'alltags')
? []
: tags.split(' ')
} }
handleClickTagListItem (name) { handleClickTagListItem (name) {
@@ -139,19 +206,74 @@ class SideNav extends React.Component {
router.push(`/tags/${name}`) router.push(`/tags/${name}`)
} }
handleSortTagsByChange (e) {
const { dispatch } = this.props
const config = {
sortTagsBy: e.target.value
}
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
}
handleClickNarrowToTag (tag) {
const { router } = this.context
const { location } = this.props
const listOfTags = this.getActiveTags(location.pathname)
const indexOfTag = listOfTags.indexOf(tag)
if (indexOfTag > -1) {
listOfTags.splice(indexOfTag, 1)
} else {
listOfTags.push(tag)
}
router.push(`/tags/${listOfTags.join(' ')}`)
}
emptyTrash (entries) {
const { dispatch } = this.props
const deletionPromises = entries.map((note) => {
return dataApi.deleteNote(note.storage, note.key)
})
Promise.all(deletionPromises)
.then((arrayOfStorageAndNoteKeys) => {
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
})
})
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
console.log('Trash emptied')
}
handleFilterButtonContextMenu (event) {
const { data } = this.props
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
context.popup([
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
])
}
render () { render () {
const { data, location, config, dispatch } = this.props const { data, location, config, dispatch } = this.props
const isFolded = config.isSideNavFolded const isFolded = config.isSideNavFolded
const storageList = data.storageMap.map((storage, key) => { const storageList = data.storageMap.map((storage, key) => {
return <StorageItem const SortableStorageItem = SortableContainer(StorageItem)
return <SortableStorageItem
key={storage.key} key={storage.key}
storage={storage} storage={storage}
data={data} data={data}
location={location} location={location}
isFolded={isFolded} isFolded={isFolded}
dispatch={dispatch} dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
/> />
}) })
const style = {} const style = {}

View File

@@ -69,3 +69,14 @@ body[data-theme="dark"]
navDarkButtonColor() navDarkButtonColor()
border-color $ui-dark-borderColor border-color $ui-dark-borderColor
border-left 1px solid $ui-dark-borderColor border-left 1px solid $ui-dark-borderColor
body[data-theme="monokai"]
navButtonColor()
.zoom
border-color $ui-dark-borderColor
color $ui-monokai-text-color
&:hover
transition 0.15s
color $ui-monokai-active-color
&:active
color $ui-monokai-active-color

View File

@@ -3,10 +3,12 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl' import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager' import ZoomManager from 'browser/main/lib/ZoomManager'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
const electron = require('electron') const electron = require('electron')
const { remote, ipcRenderer } = electron const { remote, ipcRenderer } = electron
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0] const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
@@ -14,9 +16,9 @@ class StatusBar extends React.Component {
updateApp () { updateApp () {
const index = dialog.showMessageBox(remote.getCurrentWindow(), { const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Update Boostnote', message: i18n.__('Update Boostnote'),
detail: 'New Boostnote is ready to be installed.', detail: i18n.__('New Boostnote is ready to be installed.'),
buttons: ['Restart & Install', 'Not Now'] buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
}) })
if (index === 0) { if (index === 0) {
@@ -25,16 +27,16 @@ class StatusBar extends React.Component {
} }
handleZoomButtonClick (e) { handleZoomButtonClick (e) {
const menu = new Menu() const templates = []
zoomOptions.forEach((zoom) => { zoomOptions.forEach((zoom) => {
menu.append(new MenuItem({ templates.push({
label: Math.floor(zoom * 100) + '%', label: Math.floor(zoom * 100) + '%',
click: () => this.handleZoomMenuItemClick(zoom) click: () => this.handleZoomMenuItemClick(zoom)
})) })
}) })
menu.popup(remote.getCurrentWindow()) context.popup(templates)
} }
handleZoomMenuItemClick (zoomFactor) { handleZoomMenuItemClick (zoomFactor) {
@@ -62,7 +64,7 @@ class StatusBar extends React.Component {
{status.updateReady {status.updateReady
? <button onClick={this.updateApp} styleName='update'> ? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update! <i styleName='update-icon' className='fa fa-cloud-download' /> {i18n.__('Ready to Update!')}
</button> </button>
: null : null
} }

View File

@@ -40,6 +40,32 @@ $control-height = 34px
padding-bottom 2px padding-bottom 2px
background-color $ui-noteList-backgroundColor background-color $ui-noteList-backgroundColor
.control-search-input-clear
height 16px
width 16px
position absolute
right 40px
top 10px
z-index 300
border none
background-color transparent
color #999
&:hover .control-search-input-clear-tooltip
opacity 1
.control-search-input-clear-tooltip
tooltip()
position fixed
pointer-events none
top 50px
left 433px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.control-search-optionList .control-search-optionList
position fixed position fixed
z-index 200 z-index 200
@@ -208,3 +234,25 @@ body[data-theme="solarized-dark"]
input input
background-color $ui-solarized-dark-noteList-backgroundColor background-color $ui-solarized-dark-noteList-backgroundColor
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
body[data-theme="monokai"]
.root, .root--expanded
background-color $ui-monokai-noteList-backgroundColor
.control
border-color $ui-monokai-borderColor
.control-search
background-color $ui-monokai-noteList-backgroundColor
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color $ui-monokai-inactive-text-color
background-color $ui-monokai-noteList-backgroundColor
.control-search-input
background-color $ui-monokai-noteList-backgroundColor
input
background-color $ui-monokai-noteList-backgroundColor
color $ui-monokai-text-color

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