mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 01:36:22 +00:00
Merge branch 'master' into theme-nord
This commit is contained in:
2
.babelrc
2
.babelrc
@@ -7,7 +7,7 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"presets": ["env" ,"react", "es2015"],
|
"presets": ["env" ,"react", "es2015"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
[ "babel-plugin-webpack-alias", { "config": "<rootDir>/webpack.config.js" } ]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
"globals": {
|
"globals": {
|
||||||
"FileReader": true,
|
"FileReader": true,
|
||||||
"localStorage": true,
|
"localStorage": true,
|
||||||
"fetch": true
|
"fetch": true,
|
||||||
|
"Image": true,
|
||||||
|
"MutationObserver": true
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"jest": true
|
"jest": true
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
issuehunt: BoostIo/Boostnote
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,4 +9,5 @@ node_modules/*
|
|||||||
/secret
|
/secret
|
||||||
*.log
|
*.log
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 7
|
- 8
|
||||||
script:
|
script:
|
||||||
- npm run lint && npm run test
|
- npm run lint && npm run test
|
||||||
- yarn jest
|
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@6.4 && grunt pre-build; fi'
|
||||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
|
||||||
after_success:
|
after_success:
|
||||||
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
||||||
-in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d
|
-in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d
|
||||||
|
|||||||
@@ -14,18 +14,21 @@ import {
|
|||||||
import TextEditorInterface from 'browser/lib/TextEditorInterface'
|
import TextEditorInterface from 'browser/lib/TextEditorInterface'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import iconv from 'iconv-lite'
|
import iconv from 'iconv-lite'
|
||||||
import crypto from 'crypto'
|
|
||||||
import consts from 'browser/lib/consts'
|
import { isMarkdownTitleURL } from 'browser/lib/utils'
|
||||||
import styles from '../components/CodeEditor.styl'
|
import styles from '../components/CodeEditor.styl'
|
||||||
import fs from 'fs'
|
|
||||||
const { ipcRenderer, remote, clipboard } = require('electron')
|
const { ipcRenderer, remote, clipboard } = require('electron')
|
||||||
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||||
const spellcheck = require('browser/lib/spellcheck')
|
const spellcheck = require('browser/lib/spellcheck')
|
||||||
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
|
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu
|
||||||
import TurndownService from 'turndown'
|
import { createTurndownService } from '../lib/turndown'
|
||||||
import {
|
import {languageMaps} from '../lib/CMLanguageList'
|
||||||
gfm
|
import snippetManager from '../lib/SnippetManager'
|
||||||
} from 'turndown-plugin-gfm'
|
import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator'
|
||||||
|
import markdownlint from 'markdownlint'
|
||||||
|
import Jsonlint from 'jsonlint-mod'
|
||||||
|
import { DEFAULT_CONFIG } from '../main/lib/ConfigManager'
|
||||||
|
import prettier from 'prettier'
|
||||||
|
|
||||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||||
|
|
||||||
@@ -38,85 +41,6 @@ function translateHotkey (hotkey) {
|
|||||||
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
|
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageMaps = {
|
|
||||||
brainfuck: 'Brainfuck',
|
|
||||||
cpp: 'C++',
|
|
||||||
cs: 'C#',
|
|
||||||
clojure: 'Clojure',
|
|
||||||
'clojure-repl': 'ClojureScript',
|
|
||||||
cmake: 'CMake',
|
|
||||||
coffeescript: 'CoffeeScript',
|
|
||||||
crystal: 'Crystal',
|
|
||||||
css: 'CSS',
|
|
||||||
d: 'D',
|
|
||||||
dart: 'Dart',
|
|
||||||
delphi: 'Pascal',
|
|
||||||
diff: 'Diff',
|
|
||||||
django: 'Django',
|
|
||||||
dockerfile: 'Dockerfile',
|
|
||||||
ebnf: 'EBNF',
|
|
||||||
elm: 'Elm',
|
|
||||||
erlang: 'Erlang',
|
|
||||||
'erlang-repl': 'Erlang',
|
|
||||||
fortran: 'Fortran',
|
|
||||||
fsharp: 'F#',
|
|
||||||
gherkin: 'Gherkin',
|
|
||||||
go: 'Go',
|
|
||||||
groovy: 'Groovy',
|
|
||||||
haml: 'HAML',
|
|
||||||
haskell: 'Haskell',
|
|
||||||
haxe: 'Haxe',
|
|
||||||
http: 'HTTP',
|
|
||||||
ini: 'toml',
|
|
||||||
java: 'Java',
|
|
||||||
javascript: 'JavaScript',
|
|
||||||
json: 'JSON',
|
|
||||||
julia: 'Julia',
|
|
||||||
kotlin: 'Kotlin',
|
|
||||||
less: 'LESS',
|
|
||||||
livescript: 'LiveScript',
|
|
||||||
lua: 'Lua',
|
|
||||||
markdown: 'Markdown',
|
|
||||||
mathematica: 'Mathematica',
|
|
||||||
nginx: 'Nginx',
|
|
||||||
nsis: 'NSIS',
|
|
||||||
objectivec: 'Objective-C',
|
|
||||||
ocaml: 'Ocaml',
|
|
||||||
perl: 'Perl',
|
|
||||||
php: 'PHP',
|
|
||||||
powershell: 'PowerShell',
|
|
||||||
properties: 'Properties files',
|
|
||||||
protobuf: 'ProtoBuf',
|
|
||||||
python: 'Python',
|
|
||||||
puppet: 'Puppet',
|
|
||||||
q: 'Q',
|
|
||||||
r: 'R',
|
|
||||||
ruby: 'Ruby',
|
|
||||||
rust: 'Rust',
|
|
||||||
sas: 'SAS',
|
|
||||||
scala: 'Scala',
|
|
||||||
scheme: 'Scheme',
|
|
||||||
scss: 'SCSS',
|
|
||||||
shell: 'Shell',
|
|
||||||
smalltalk: 'Smalltalk',
|
|
||||||
sml: 'SML',
|
|
||||||
sql: 'SQL',
|
|
||||||
stylus: 'Stylus',
|
|
||||||
swift: 'Swift',
|
|
||||||
tcl: 'Tcl',
|
|
||||||
tex: 'LaTex',
|
|
||||||
typescript: 'TypeScript',
|
|
||||||
twig: 'Twig',
|
|
||||||
vbnet: 'VB.NET',
|
|
||||||
vbscript: 'VBScript',
|
|
||||||
verilog: 'Verilog',
|
|
||||||
vhdl: 'VHDL',
|
|
||||||
xml: 'HTML',
|
|
||||||
xquery: 'XQuery',
|
|
||||||
yaml: 'YAML',
|
|
||||||
elixir: 'Elixir'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CodeEditor extends React.Component {
|
export default class CodeEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -130,6 +54,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.focusHandler = () => {
|
this.focusHandler = () => {
|
||||||
ipcRenderer.send('editor:focused', true)
|
ipcRenderer.send('editor:focused', true)
|
||||||
}
|
}
|
||||||
|
const debouncedDeletionOfAttachments = _.debounce(attachmentManagement.deleteAttachmentsNotPresentInNote, 30000)
|
||||||
this.blurHandler = (editor, e) => {
|
this.blurHandler = (editor, e) => {
|
||||||
ipcRenderer.send('editor:focused', false)
|
ipcRenderer.send('editor:focused', false)
|
||||||
if (e == null) return null
|
if (e == null) return null
|
||||||
@@ -141,16 +66,13 @@ 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 {
|
const {
|
||||||
storageKey,
|
storageKey,
|
||||||
noteKey
|
noteKey
|
||||||
} = this.props
|
} = this.props
|
||||||
attachmentManagement.deleteAttachmentsNotPresentInNote(
|
if (this.props.deleteUnusedAttachments === true) {
|
||||||
this.editor.getValue(),
|
debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey)
|
||||||
storageKey,
|
}
|
||||||
noteKey
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
this.pasteHandler = (editor, e) => {
|
this.pasteHandler = (editor, e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -163,6 +85,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||||
this.searchState = null
|
this.searchState = null
|
||||||
this.scrollToLineHandeler = this.scrollToLine.bind(this)
|
this.scrollToLineHandeler = this.scrollToLine.bind(this)
|
||||||
|
this.getCodeEditorLintConfig = this.getCodeEditorLintConfig.bind(this)
|
||||||
|
this.validatorOfMarkdown = this.validatorOfMarkdown.bind(this)
|
||||||
|
|
||||||
this.formatTable = () => this.handleFormatTable()
|
this.formatTable = () => this.handleFormatTable()
|
||||||
|
|
||||||
@@ -177,7 +101,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
this.editorActivityHandler = () => this.handleEditorActivity()
|
this.editorActivityHandler = () => this.handleEditorActivity()
|
||||||
|
|
||||||
this.turndownService = new TurndownService()
|
this.turndownService = createTurndownService()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearch (msg) {
|
handleSearch (msg) {
|
||||||
@@ -185,7 +109,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
const component = this
|
const component = this
|
||||||
|
|
||||||
if (component.searchState) cm.removeOverlay(component.searchState)
|
if (component.searchState) cm.removeOverlay(component.searchState)
|
||||||
if (msg.length < 3) return
|
if (msg.length < 1) return
|
||||||
|
|
||||||
cm.operation(function () {
|
cm.operation(function () {
|
||||||
component.searchState = makeOverlay(msg, 'searching')
|
component.searchState = makeOverlay(msg, 'searching')
|
||||||
@@ -228,7 +152,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
updateDefaultKeyMap () {
|
updateDefaultKeyMap () {
|
||||||
const { hotkey } = this.props
|
const { hotkey } = this.props
|
||||||
const expandSnippet = this.expandSnippet.bind(this)
|
const self = this
|
||||||
|
const expandSnippet = snippetManager.expandSnippet
|
||||||
|
|
||||||
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
|
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
|
||||||
Tab: function (cm) {
|
Tab: function (cm) {
|
||||||
@@ -248,14 +173,16 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
cm.execCommand('goLineEnd')
|
cm.execCommand('goLineEnd')
|
||||||
} else if (
|
} else if (
|
||||||
!charBeforeCursor.match(/\t|\s|\r|\n/) &&
|
!charBeforeCursor.match(/\t|\s|\r|\n|\$/) &&
|
||||||
cursor.ch > 1
|
cursor.ch > 1
|
||||||
) {
|
) {
|
||||||
// text expansion on tab key if the char before is alphabet
|
// text expansion on tab key if the char before is alphabet
|
||||||
const snippets = JSON.parse(
|
const wordBeforeCursor = self.getWordBeforeCursor(
|
||||||
fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
|
line,
|
||||||
|
cursor.line,
|
||||||
|
cursor.ch
|
||||||
)
|
)
|
||||||
if (expandSnippet(line, cursor, cm, snippets) === false) {
|
if (expandSnippet(wordBeforeCursor, cursor, cm) === false) {
|
||||||
if (tabs) {
|
if (tabs) {
|
||||||
cm.execCommand('insertTab')
|
cm.execCommand('insertTab')
|
||||||
} else {
|
} else {
|
||||||
@@ -277,6 +204,14 @@ export default class CodeEditor extends React.Component {
|
|||||||
'Cmd-T': function (cm) {
|
'Cmd-T': function (cm) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
},
|
},
|
||||||
|
[translateHotkey(hotkey.insertDate)]: function (cm) {
|
||||||
|
const dateNow = new Date()
|
||||||
|
cm.replaceSelection(dateNow.toLocaleDateString())
|
||||||
|
},
|
||||||
|
[translateHotkey(hotkey.insertDateTime)]: function (cm) {
|
||||||
|
const dateNow = new Date()
|
||||||
|
cm.replaceSelection(dateNow.toLocaleString())
|
||||||
|
},
|
||||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||||
'Ctrl-C': cm => {
|
'Ctrl-C': cm => {
|
||||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||||
@@ -284,6 +219,37 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
return CodeMirror.Pass
|
return CodeMirror.Pass
|
||||||
},
|
},
|
||||||
|
[translateHotkey(hotkey.prettifyMarkdown)]: cm => {
|
||||||
|
// Default / User configured prettier options
|
||||||
|
const currentConfig = JSON.parse(self.props.prettierConfig)
|
||||||
|
|
||||||
|
// Parser type will always need to be markdown so we override the option before use
|
||||||
|
currentConfig.parser = 'markdown'
|
||||||
|
|
||||||
|
// Get current cursor position
|
||||||
|
const cursorPos = cm.getCursor()
|
||||||
|
currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos)
|
||||||
|
|
||||||
|
// Prettify contents of editor
|
||||||
|
const formattedTextDetails = prettier.formatWithCursor(cm.doc.getValue(), currentConfig)
|
||||||
|
|
||||||
|
const formattedText = formattedTextDetails.formatted
|
||||||
|
const formattedCursorPos = formattedTextDetails.cursorOffset
|
||||||
|
cm.doc.setValue(formattedText)
|
||||||
|
|
||||||
|
// Reset Cursor position to be at the same markdown as was before prettifying
|
||||||
|
const newCursorPos = cm.doc.posFromIndex(formattedCursorPos)
|
||||||
|
cm.doc.setCursor(newCursorPos)
|
||||||
|
},
|
||||||
|
[translateHotkey(hotkey.sortLines)]: cm => {
|
||||||
|
const selection = cm.doc.getSelection()
|
||||||
|
const appendLineBreak = /\n$/.test(selection)
|
||||||
|
|
||||||
|
const sorted = _.split(selection.trim(), '\n').sort()
|
||||||
|
const sortedString = _.join(sorted, '\n') + (appendLineBreak ? '\n' : '')
|
||||||
|
|
||||||
|
cm.doc.replaceSelection(sortedString)
|
||||||
|
},
|
||||||
[translateHotkey(hotkey.pasteSmartly)]: cm => {
|
[translateHotkey(hotkey.pasteSmartly)]: cm => {
|
||||||
this.handlePaste(cm, true)
|
this.handlePaste(cm, true)
|
||||||
}
|
}
|
||||||
@@ -307,25 +273,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { rulers, enableRulers } = this.props
|
const { rulers, enableRulers, enableMarkdownLint, RTL } = this.props
|
||||||
eventEmitter.on('line:jump', this.scrollToLineHandeler)
|
eventEmitter.on('line:jump', this.scrollToLineHandeler)
|
||||||
|
|
||||||
const defaultSnippet = [
|
snippetManager.init()
|
||||||
{
|
|
||||||
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.updateDefaultKeyMap()
|
this.updateDefaultKeyMap()
|
||||||
|
|
||||||
this.value = this.props.value
|
this.value = this.props.value
|
||||||
@@ -334,7 +285,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
linesHighlighted: this.props.linesHighlighted,
|
linesHighlighted: this.props.linesHighlighted,
|
||||||
lineNumbers: this.props.displayLineNumbers,
|
lineNumbers: this.props.displayLineNumbers,
|
||||||
lineWrapping: true,
|
lineWrapping: this.props.lineWrapping,
|
||||||
theme: this.props.theme,
|
theme: this.props.theme,
|
||||||
indentUnit: this.props.indentSize,
|
indentUnit: this.props.indentSize,
|
||||||
tabSize: this.props.indentSize,
|
tabSize: this.props.indentSize,
|
||||||
@@ -343,17 +294,23 @@ export default class CodeEditor extends React.Component {
|
|||||||
scrollPastEnd: this.props.scrollPastEnd,
|
scrollPastEnd: this.props.scrollPastEnd,
|
||||||
inputStyle: 'textarea',
|
inputStyle: 'textarea',
|
||||||
dragDrop: false,
|
dragDrop: false,
|
||||||
|
direction: RTL ? 'rtl' : 'ltr',
|
||||||
|
rtlMoveVisually: RTL,
|
||||||
foldGutter: true,
|
foldGutter: true,
|
||||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false,
|
||||||
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||||
autoCloseBrackets: {
|
autoCloseBrackets: {
|
||||||
pairs: this.props.matchingPairs,
|
pairs: this.props.matchingPairs,
|
||||||
triples: this.props.matchingTriples,
|
triples: this.props.matchingTriples,
|
||||||
explode: this.props.explodingPairs,
|
explode: this.props.explodingPairs,
|
||||||
override: true
|
override: true
|
||||||
},
|
},
|
||||||
extraKeys: this.defaultKeyMap
|
extraKeys: this.defaultKeyMap,
|
||||||
|
prettierConfig: this.props.prettierConfig
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none'
|
||||||
|
|
||||||
if (!this.props.mode && this.props.value && this.props.autoDetect) {
|
if (!this.props.mode && this.props.value && this.props.autoDetect) {
|
||||||
this.autoDetectLanguage(this.props.value)
|
this.autoDetectLanguage(this.props.value)
|
||||||
} else {
|
} else {
|
||||||
@@ -520,61 +477,12 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.initialHighlighting()
|
this.initialHighlighting()
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
let cursorIndex
|
|
||||||
for (let j = 0; j < snippetLines.length; j++) {
|
|
||||||
cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
|
||||||
|
|
||||||
if (cursorIndex !== -1) {
|
|
||||||
cursorLineNumber = j
|
|
||||||
cursorLinePosition = cursorIndex
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cm.replaceRange(
|
|
||||||
snippets[i].content.replace(templateCursorString, ''),
|
|
||||||
wordBeforeCursor.range.from,
|
|
||||||
wordBeforeCursor.range.to
|
|
||||||
)
|
|
||||||
cm.setCursor({
|
|
||||||
line: cursor.line + cursorLineNumber,
|
|
||||||
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
cm.replaceRange(
|
|
||||||
snippets[i].content,
|
|
||||||
wordBeforeCursor.range.from,
|
|
||||||
wordBeforeCursor.range.to
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
getWordBeforeCursor (line, lineNumber, cursorPosition) {
|
getWordBeforeCursor (line, lineNumber, cursorPosition) {
|
||||||
let wordBeforeCursor = ''
|
let wordBeforeCursor = ''
|
||||||
const originCursorPosition = cursorPosition
|
const originCursorPosition = cursorPosition
|
||||||
const emptyChars = /\t|\s|\r|\n/
|
const emptyChars = /\t|\s|\r|\n|\$/
|
||||||
|
|
||||||
// to prevent the word to expand is long that will crash the whole app
|
// to prevent the word is long that will crash the whole app
|
||||||
// the safeStop is there to stop user to expand words that longer than 20 chars
|
// the safeStop is there to stop user to expand words that longer than 20 chars
|
||||||
const safeStop = 20
|
const safeStop = 20
|
||||||
|
|
||||||
@@ -584,7 +492,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
if (!emptyChars.test(currentChar)) {
|
if (!emptyChars.test(currentChar)) {
|
||||||
wordBeforeCursor = currentChar + wordBeforeCursor
|
wordBeforeCursor = currentChar + wordBeforeCursor
|
||||||
} else if (wordBeforeCursor.length >= safeStop) {
|
} else if (wordBeforeCursor.length >= safeStop) {
|
||||||
throw new Error('Your snippet trigger is too long !')
|
throw new Error('Stopped after 20 loops for safety reason !')
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -629,7 +537,9 @@ export default class CodeEditor extends React.Component {
|
|||||||
let needRefresh = false
|
let needRefresh = false
|
||||||
const {
|
const {
|
||||||
rulers,
|
rulers,
|
||||||
enableRulers
|
enableRulers,
|
||||||
|
enableMarkdownLint,
|
||||||
|
customMarkdownLintConfig
|
||||||
} = this.props
|
} = this.props
|
||||||
if (prevProps.mode !== this.props.mode) {
|
if (prevProps.mode !== this.props.mode) {
|
||||||
this.setMode(this.props.mode)
|
this.setMode(this.props.mode)
|
||||||
@@ -647,6 +557,20 @@ export default class CodeEditor extends React.Component {
|
|||||||
if (prevProps.keyMap !== this.props.keyMap) {
|
if (prevProps.keyMap !== this.props.keyMap) {
|
||||||
needRefresh = true
|
needRefresh = true
|
||||||
}
|
}
|
||||||
|
if (prevProps.RTL !== this.props.RTL) {
|
||||||
|
this.editor.setOption('direction', this.props.RTL ? 'rtl' : 'ltr')
|
||||||
|
this.editor.setOption('rtlMoveVisually', this.props.RTL)
|
||||||
|
}
|
||||||
|
if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) {
|
||||||
|
if (!enableMarkdownLint) {
|
||||||
|
this.editor.setOption('lint', {default: false})
|
||||||
|
document.querySelector('.CodeMirror-lint-markers').style.display = 'none'
|
||||||
|
} else {
|
||||||
|
this.editor.setOption('lint', this.getCodeEditorLintConfig())
|
||||||
|
document.querySelector('.CodeMirror-lint-markers').style.display = 'inline-block'
|
||||||
|
}
|
||||||
|
needRefresh = true
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevProps.enableRulers !== enableRulers ||
|
prevProps.enableRulers !== enableRulers ||
|
||||||
@@ -667,6 +591,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
|
this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.lineWrapping !== this.props.lineWrapping) {
|
||||||
|
this.editor.setOption('lineWrapping', this.props.lineWrapping)
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -721,12 +649,65 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
|
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments) {
|
||||||
|
this.editor.setOption('deleteUnusedAttachments', this.props.deleteUnusedAttachments)
|
||||||
|
}
|
||||||
|
|
||||||
if (needRefresh) {
|
if (needRefresh) {
|
||||||
this.editor.refresh()
|
this.editor.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCodeEditorLintConfig () {
|
||||||
|
const { mode } = this.props
|
||||||
|
const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown'
|
||||||
|
|
||||||
|
return checkMarkdownNoteIsOpen ? {
|
||||||
|
getAnnotations: this.validatorOfMarkdown,
|
||||||
|
async: true
|
||||||
|
} : false
|
||||||
|
}
|
||||||
|
|
||||||
|
validatorOfMarkdown (text, updateLinting) {
|
||||||
|
const { customMarkdownLintConfig } = this.props
|
||||||
|
let lintConfigJson
|
||||||
|
try {
|
||||||
|
Jsonlint.parse(customMarkdownLintConfig)
|
||||||
|
lintConfigJson = JSON.parse(customMarkdownLintConfig)
|
||||||
|
} catch (err) {
|
||||||
|
eventEmitter.emit('APP_SETTING_ERROR')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const lintOptions = {
|
||||||
|
strings: {
|
||||||
|
content: text
|
||||||
|
},
|
||||||
|
config: lintConfigJson
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdownlint(lintOptions, (err, result) => {
|
||||||
|
if (!err) {
|
||||||
|
const foundIssues = []
|
||||||
|
const splitText = text.split('\n')
|
||||||
|
result.content.map(item => {
|
||||||
|
let ruleNames = ''
|
||||||
|
item.ruleNames.map((ruleName, index) => {
|
||||||
|
ruleNames += ruleName
|
||||||
|
ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/'
|
||||||
|
})
|
||||||
|
const lineNumber = item.lineNumber - 1
|
||||||
|
foundIssues.push({
|
||||||
|
from: CodeMirror.Pos(lineNumber, 0),
|
||||||
|
to: CodeMirror.Pos(lineNumber, splitText[lineNumber].length),
|
||||||
|
message: ruleNames + item.ruleDescription,
|
||||||
|
severity: 'warning'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
updateLinting(foundIssues)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
setMode (mode) {
|
setMode (mode) {
|
||||||
let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text'))
|
let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text'))
|
||||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
@@ -738,6 +719,34 @@ export default class CodeEditor extends React.Component {
|
|||||||
handleChange (editor, changeObject) {
|
handleChange (editor, changeObject) {
|
||||||
spellcheck.handleChange(editor, changeObject)
|
spellcheck.handleChange(editor, changeObject)
|
||||||
|
|
||||||
|
// The current note contains an toc. We'll check for changes on headlines.
|
||||||
|
// origin is undefined when markdownTocGenerator replace the old tod
|
||||||
|
if (tocExistsInEditor(editor) && changeObject.origin !== undefined) {
|
||||||
|
let requireTocUpdate
|
||||||
|
|
||||||
|
// Check if one of the changed lines contains a headline
|
||||||
|
for (let line = 0; line < changeObject.text.length; line++) {
|
||||||
|
if (this.linePossibleContainsHeadline(editor.getLine(changeObject.from.line + line))) {
|
||||||
|
requireTocUpdate = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requireTocUpdate) {
|
||||||
|
// Check if one of the removed lines contains a headline
|
||||||
|
for (let line = 0; line < changeObject.removed.length; line++) {
|
||||||
|
if (this.linePossibleContainsHeadline(changeObject.removed[line])) {
|
||||||
|
requireTocUpdate = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireTocUpdate) {
|
||||||
|
generateInEditor(editor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.updateHighlight(editor, changeObject)
|
this.updateHighlight(editor, changeObject)
|
||||||
|
|
||||||
this.value = editor.getValue()
|
this.value = editor.getValue()
|
||||||
@@ -746,15 +755,21 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
linePossibleContainsHeadline (currentLine) {
|
||||||
|
// We can't check if the line start with # because when some write text before
|
||||||
|
// the # we also need to update the toc
|
||||||
|
return currentLine.includes('# ')
|
||||||
|
}
|
||||||
|
|
||||||
incrementLines (start, linesAdded, linesRemoved, editor) {
|
incrementLines (start, linesAdded, linesRemoved, editor) {
|
||||||
let highlightedLines = editor.options.linesHighlighted
|
const highlightedLines = editor.options.linesHighlighted
|
||||||
|
|
||||||
const totalHighlightedLines = highlightedLines.length
|
const totalHighlightedLines = highlightedLines.length
|
||||||
|
|
||||||
let offset = linesAdded - linesRemoved
|
const offset = linesAdded - linesRemoved
|
||||||
|
|
||||||
// Store new items to be added as we're changing the lines
|
// Store new items to be added as we're changing the lines
|
||||||
let newLines = []
|
const newLines = []
|
||||||
|
|
||||||
let i = totalHighlightedLines
|
let i = totalHighlightedLines
|
||||||
|
|
||||||
@@ -835,6 +850,9 @@ export default class CodeEditor extends React.Component {
|
|||||||
ch: 1
|
ch: 1
|
||||||
}
|
}
|
||||||
this.editor.setCursor(cursor)
|
this.editor.setCursor(cursor)
|
||||||
|
const top = this.editor.charCoords({line: num, ch: 0}, 'local').top
|
||||||
|
const middleHeight = this.editor.getScrollerElement().offsetHeight / 2
|
||||||
|
this.editor.scrollTo(null, top - middleHeight - 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
@@ -862,6 +880,17 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.setCursor(cursor)
|
this.editor.setCursor(cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update content of one line
|
||||||
|
* @param {Number} lineNumber
|
||||||
|
* @param {String} content
|
||||||
|
*/
|
||||||
|
setLineContent (lineNumber, content) {
|
||||||
|
const prevContent = this.editor.getLine(lineNumber)
|
||||||
|
const prevContentLength = prevContent ? prevContent.length : 0
|
||||||
|
this.editor.replaceRange(content, { line: lineNumber, ch: 0 }, { line: lineNumber, ch: prevContentLength })
|
||||||
|
}
|
||||||
|
|
||||||
handleDropImage (dropEvent) {
|
handleDropImage (dropEvent) {
|
||||||
dropEvent.preventDefault()
|
dropEvent.preventDefault()
|
||||||
const {
|
const {
|
||||||
@@ -950,6 +979,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
if (isInFencedCodeBlock(editor)) {
|
if (isInFencedCodeBlock(editor)) {
|
||||||
this.handlePasteText(editor, pastedTxt)
|
this.handlePasteText(editor, pastedTxt)
|
||||||
|
} else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||||
|
this.handlePasteUrl(editor, pastedTxt)
|
||||||
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||||
this.handlePasteUrl(editor, pastedTxt)
|
this.handlePasteUrl(editor, pastedTxt)
|
||||||
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||||
@@ -991,7 +1022,17 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePasteUrl (editor, pastedTxt) {
|
handlePasteUrl (editor, pastedTxt) {
|
||||||
const taggedUrl = `<${pastedTxt}>`
|
let taggedUrl = `<${pastedTxt}>`
|
||||||
|
let urlToFetch = pastedTxt
|
||||||
|
let titleMark = ''
|
||||||
|
|
||||||
|
if (isMarkdownTitleURL(pastedTxt)) {
|
||||||
|
const pastedTxtSplitted = pastedTxt.split(' ')
|
||||||
|
titleMark = `${pastedTxtSplitted[0]} `
|
||||||
|
urlToFetch = pastedTxtSplitted[1]
|
||||||
|
taggedUrl = `<${urlToFetch}>`
|
||||||
|
}
|
||||||
|
|
||||||
editor.replaceSelection(taggedUrl)
|
editor.replaceSelection(taggedUrl)
|
||||||
|
|
||||||
const isImageReponse = response => {
|
const isImageReponse = response => {
|
||||||
@@ -1003,22 +1044,23 @@ export default class CodeEditor extends React.Component {
|
|||||||
const replaceTaggedUrl = replacement => {
|
const replaceTaggedUrl = replacement => {
|
||||||
const value = editor.getValue()
|
const value = editor.getValue()
|
||||||
const cursor = editor.getCursor()
|
const cursor = editor.getCursor()
|
||||||
const newValue = value.replace(taggedUrl, replacement)
|
const newValue = value.replace(taggedUrl, titleMark + replacement)
|
||||||
const newCursor = Object.assign({}, cursor, {
|
const newCursor = Object.assign({}, cursor, {
|
||||||
ch: cursor.ch + newValue.length - value.length
|
ch: cursor.ch + newValue.length - (value.length - titleMark.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.setValue(newValue)
|
editor.setValue(newValue)
|
||||||
editor.setCursor(newCursor)
|
editor.setCursor(newCursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(pastedTxt, {
|
fetch(urlToFetch, {
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (isImageReponse(response)) {
|
if (isImageReponse(response)) {
|
||||||
return this.mapImageResponse(response, pastedTxt)
|
return this.mapImageResponse(response, urlToFetch)
|
||||||
} else {
|
} else {
|
||||||
return this.mapNormalResponse(response, pastedTxt)
|
return this.mapNormalResponse(response, urlToFetch)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(replacement => {
|
.then(replacement => {
|
||||||
@@ -1138,13 +1180,11 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
ref='root'
|
ref='root'
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
style={
|
style={{
|
||||||
{
|
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
width: width
|
width: width
|
||||||
}
|
}}
|
||||||
}
|
|
||||||
onDrop={
|
onDrop={
|
||||||
e => this.handleDropImage(e)
|
e => this.handleDropImage(e)
|
||||||
}
|
}
|
||||||
@@ -1182,7 +1222,11 @@ CodeEditor.propTypes = {
|
|||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
readOnly: PropTypes.bool,
|
readOnly: PropTypes.bool,
|
||||||
autoDetect: PropTypes.bool,
|
autoDetect: PropTypes.bool,
|
||||||
spellCheck: PropTypes.bool
|
spellCheck: PropTypes.bool,
|
||||||
|
enableMarkdownLint: PropTypes.bool,
|
||||||
|
customMarkdownLintConfig: PropTypes.string,
|
||||||
|
deleteUnusedAttachments: PropTypes.bool,
|
||||||
|
RTL: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
CodeEditor.defaultProps = {
|
CodeEditor.defaultProps = {
|
||||||
@@ -1194,5 +1238,10 @@ CodeEditor.defaultProps = {
|
|||||||
indentSize: 4,
|
indentSize: 4,
|
||||||
indentType: 'space',
|
indentType: 'space',
|
||||||
autoDetect: false,
|
autoDetect: false,
|
||||||
spellCheck: false
|
spellCheck: false,
|
||||||
|
enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint,
|
||||||
|
customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig,
|
||||||
|
prettierConfig: DEFAULT_CONFIG.editor.prettierConfig,
|
||||||
|
deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments,
|
||||||
|
RTL: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,3 @@
|
|||||||
|
|
||||||
.spellcheck-select
|
.spellcheck-select
|
||||||
border: none
|
border: none
|
||||||
text-decoration underline wavy red
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.value = this.refs.code.value
|
this.value = this.refs.code.value
|
||||||
eventEmitter.on('editor:lock', this.lockEditorCode)
|
eventEmitter.on('editor:lock', this.lockEditorCode)
|
||||||
|
eventEmitter.on('editor:focus', this.focusEditor.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate () {
|
||||||
@@ -47,6 +48,15 @@ class MarkdownEditor extends React.Component {
|
|||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.cancelQueue()
|
this.cancelQueue()
|
||||||
eventEmitter.off('editor:lock', this.lockEditorCode)
|
eventEmitter.off('editor:lock', this.lockEditorCode)
|
||||||
|
eventEmitter.off('editor:focus', this.focusEditor.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
focusEditor () {
|
||||||
|
this.setState({
|
||||||
|
status: 'CODE'
|
||||||
|
}, () => {
|
||||||
|
this.refs.code.focus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
queueRendering (value) {
|
queueRendering (value) {
|
||||||
@@ -109,7 +119,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
status: 'PREVIEW'
|
status: 'PREVIEW'
|
||||||
}, () => {
|
}, () => {
|
||||||
this.refs.preview.focus()
|
this.refs.preview.focus()
|
||||||
this.refs.preview.scrollTo(cursorPosition.line)
|
this.refs.preview.scrollToRow(cursorPosition.line)
|
||||||
})
|
})
|
||||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||||
}
|
}
|
||||||
@@ -149,24 +159,25 @@ class MarkdownEditor extends React.Component {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const idMatch = /checkbox-([0-9]+)/
|
const idMatch = /checkbox-([0-9]+)/
|
||||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
|
||||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
|
||||||
const checkReplace = /\[x\]/i
|
const checkReplace = /\[x]/i
|
||||||
const uncheckReplace = /\[ \]/
|
const uncheckReplace = /\[ ]/
|
||||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
const lines = this.refs.code.value
|
const lines = this.refs.code.value
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|
||||||
const targetLine = lines[lineIndex]
|
const targetLine = lines[lineIndex]
|
||||||
|
let newLine = targetLine
|
||||||
|
|
||||||
if (targetLine.match(checkedMatch)) {
|
if (targetLine.match(checkedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
newLine = targetLine.replace(checkReplace, '[ ]')
|
||||||
}
|
}
|
||||||
if (targetLine.match(uncheckedMatch)) {
|
if (targetLine.match(uncheckedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
newLine = targetLine.replace(uncheckReplace, '[x]')
|
||||||
}
|
}
|
||||||
this.refs.code.setValue(lines.join('\n'))
|
this.refs.code.setLineContent(lineIndex, newLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +267,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props
|
const {className, value, config, storageKey, noteKey, linesHighlighted, RTL} = this.props
|
||||||
|
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
@@ -294,6 +305,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
enableRulers={config.editor.enableRulers}
|
enableRulers={config.editor.enableRulers}
|
||||||
rulers={config.editor.rulers}
|
rulers={config.editor.rulers}
|
||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
lineWrapping
|
||||||
matchingPairs={config.editor.matchingPairs}
|
matchingPairs={config.editor.matchingPairs}
|
||||||
matchingTriples={config.editor.matchingTriples}
|
matchingTriples={config.editor.matchingTriples}
|
||||||
explodingPairs={config.editor.explodingPairs}
|
explodingPairs={config.editor.explodingPairs}
|
||||||
@@ -309,6 +321,11 @@ class MarkdownEditor extends React.Component {
|
|||||||
enableSmartPaste={config.editor.enableSmartPaste}
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
hotkey={config.hotkey}
|
hotkey={config.hotkey}
|
||||||
switchPreview={config.editor.switchPreview}
|
switchPreview={config.editor.switchPreview}
|
||||||
|
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||||
|
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||||
|
prettierConfig={config.editor.prettierConfig}
|
||||||
|
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||||
|
RTL={RTL}
|
||||||
/>
|
/>
|
||||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||||
? 'preview'
|
? 'preview'
|
||||||
@@ -328,6 +345,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
smartArrows={config.preview.smartArrows}
|
smartArrows={config.preview.smartArrows}
|
||||||
breaks={config.preview.breaks}
|
breaks={config.preview.breaks}
|
||||||
sanitize={config.preview.sanitize}
|
sanitize={config.preview.sanitize}
|
||||||
|
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||||
@@ -343,6 +361,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
allowCustomCSS={config.preview.allowCustomCSS}
|
allowCustomCSS={config.preview.allowCustomCSS}
|
||||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||||
onDrop={(e) => this.handleDropImage(e)}
|
onDrop={(e) => this.handleDropImage(e)}
|
||||||
|
RTL={RTL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import consts from 'browser/lib/consts'
|
|||||||
import Raphael from 'raphael'
|
import Raphael from 'raphael'
|
||||||
import flowchart from 'flowchart'
|
import flowchart from 'flowchart'
|
||||||
import mermaidRender from './render/MermaidRender'
|
import mermaidRender from './render/MermaidRender'
|
||||||
import SequenceDiagram from 'js-sequence-diagrams'
|
import SequenceDiagram from '@rokt33r/js-sequence-diagrams'
|
||||||
import Chart from 'chart.js'
|
import Chart from 'chart.js'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||||
@@ -18,16 +18,15 @@ import mdurl from 'mdurl'
|
|||||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import context from 'browser/lib/context'
|
|
||||||
import i18n from 'browser/lib/i18n'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { render } from 'react-dom'
|
import { render } from 'react-dom'
|
||||||
import Carousel from 'react-image-carousel'
|
import Carousel from 'react-image-carousel'
|
||||||
import ConfigManager from '../main/lib/ConfigManager'
|
import ConfigManager from '../main/lib/ConfigManager'
|
||||||
import uiThemes from 'browser/lib/ui-themes'
|
import uiThemes from 'browser/lib/ui-themes'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const { remote, shell } = require('electron')
|
const { remote, shell } = require('electron')
|
||||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||||
|
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu
|
||||||
|
|
||||||
const { app } = remote
|
const { app } = remote
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
@@ -35,8 +34,6 @@ const fileUrl = require('file-url')
|
|||||||
|
|
||||||
const dialog = remote.dialog
|
const dialog = remote.dialog
|
||||||
|
|
||||||
const uri2path = require('file-uri-to-path')
|
|
||||||
|
|
||||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||||
const appPath = fileUrl(
|
const appPath = fileUrl(
|
||||||
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
||||||
@@ -47,16 +44,30 @@ const CSS_FILES = [
|
|||||||
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
|
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
|
||||||
]
|
]
|
||||||
|
|
||||||
function buildStyle (
|
/**
|
||||||
fontFamily,
|
* @param {Object} opts
|
||||||
fontSize,
|
* @param {String} opts.fontFamily
|
||||||
codeBlockFontFamily,
|
* @param {Numberl} opts.fontSize
|
||||||
lineNumber,
|
* @param {String} opts.codeBlockFontFamily
|
||||||
scrollPastEnd,
|
* @param {String} opts.theme
|
||||||
theme,
|
* @param {Boolean} [opts.lineNumber] Should show line number
|
||||||
allowCustomCSS,
|
* @param {Boolean} [opts.scrollPastEnd]
|
||||||
customCSS
|
* @param {Boolean} [opts.allowCustomCSS] Should add custom css
|
||||||
) {
|
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
function buildStyle (opts) {
|
||||||
|
const {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS,
|
||||||
|
RTL
|
||||||
|
} = opts
|
||||||
return `
|
return `
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
@@ -86,12 +97,20 @@ function buildStyle (
|
|||||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
${markdownStyle}
|
${markdownStyle}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: '${fontFamily.join("','")}';
|
font-family: '${fontFamily.join("','")}';
|
||||||
font-size: ${fontSize}px;
|
font-size: ${fontSize}px;
|
||||||
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
|
||||||
|
${scrollPastEnd ? `
|
||||||
|
padding-bottom: 90vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
${RTL ? 'direction: rtl;' : ''}
|
||||||
|
${RTL ? 'text-align: right;' : ''}
|
||||||
}
|
}
|
||||||
@media print {
|
@media print {
|
||||||
body {
|
body {
|
||||||
@@ -101,6 +120,8 @@ body {
|
|||||||
code {
|
code {
|
||||||
font-family: '${codeBlockFontFamily.join("','")}';
|
font-family: '${codeBlockFontFamily.join("','")}';
|
||||||
background-color: rgba(0,0,0,0.04);
|
background-color: rgba(0,0,0,0.04);
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
}
|
}
|
||||||
.lineNumber {
|
.lineNumber {
|
||||||
${lineNumber && 'display: block !important;'}
|
${lineNumber && 'display: block !important;'}
|
||||||
@@ -167,6 +188,10 @@ const scrollBarStyle = `
|
|||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(0, 0, 0, 0.15);
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track-piece {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
const scrollBarDarkStyle = `
|
const scrollBarDarkStyle = `
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@@ -176,6 +201,10 @@ const scrollBarDarkStyle = `
|
|||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track-piece {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
@@ -193,6 +222,19 @@ const defaultCodeBlockFontFamily = [
|
|||||||
'source-code-pro',
|
'source-code-pro',
|
||||||
'monospace'
|
'monospace'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// return the line number of the line that used to generate the specified element
|
||||||
|
// return -1 if the line is not found
|
||||||
|
function getSourceLineNumberByElement (element) {
|
||||||
|
let isHasLineNumber = element.dataset.line !== undefined
|
||||||
|
let parent = element
|
||||||
|
while (!isHasLineNumber && parent.parentElement !== null) {
|
||||||
|
parent = parent.parentElement
|
||||||
|
isHasLineNumber = parent.dataset.line !== undefined
|
||||||
|
}
|
||||||
|
return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1
|
||||||
|
}
|
||||||
|
|
||||||
export default class MarkdownPreview extends React.Component {
|
export default class MarkdownPreview extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -209,7 +251,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||||
|
this.saveAsPdfHandler = () => this.handleSaveAsPdf()
|
||||||
this.printHandler = () => this.handlePrint()
|
this.printHandler = () => this.handlePrint()
|
||||||
|
this.resizeHandler = _.throttle(this.handleResize.bind(this), 100)
|
||||||
|
|
||||||
this.linkClickHandler = this.handleLinkClick.bind(this)
|
this.linkClickHandler = this.handleLinkClick.bind(this)
|
||||||
this.initMarkdown = this.initMarkdown.bind(this)
|
this.initMarkdown = this.initMarkdown.bind(this)
|
||||||
@@ -236,30 +280,12 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenu (event) {
|
handleContextMenu (event) {
|
||||||
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
|
const menu = buildMarkdownPreviewContextMenu(this, event)
|
||||||
if (_.isFunction(this.props.onContextMenu)) {
|
const switchPreview = ConfigManager.get().editor.switchPreview
|
||||||
|
if (menu != null && switchPreview !== 'RIGHTCLICK') {
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
} else if (_.isFunction(this.props.onContextMenu)) {
|
||||||
this.props.onContextMenu(event)
|
this.props.onContextMenu(event)
|
||||||
return
|
|
||||||
}
|
|
||||||
// No contextMenu was passed to us -> execute our own link-opener
|
|
||||||
if (event.target.tagName.toLowerCase() === 'a') {
|
|
||||||
const href = event.target.href
|
|
||||||
const isLocalFile = href.startsWith('file:')
|
|
||||||
if (isLocalFile) {
|
|
||||||
const absPath = uri2path(href)
|
|
||||||
try {
|
|
||||||
if (fs.lstatSync(absPath).isFile()) {
|
|
||||||
context.popup([
|
|
||||||
{
|
|
||||||
label: i18n.__('Show in explorer'),
|
|
||||||
click: (e) => shell.showItemInFolder(absPath)
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Error while evaluating if the file is locally available', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,17 +295,27 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
handleMouseDown (e) {
|
handleMouseDown (e) {
|
||||||
const config = ConfigManager.get()
|
const config = ConfigManager.get()
|
||||||
|
const clickElement = e.target
|
||||||
|
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV"
|
||||||
|
const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
|
||||||
|
|
||||||
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
||||||
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
||||||
}
|
}
|
||||||
if (e.target != null) {
|
if (e.ctrlKey) {
|
||||||
switch (e.target.tagName) {
|
if (config.editor.type === 'SPLIT') {
|
||||||
case 'A':
|
if (lineNumber !== -1) {
|
||||||
case 'INPUT':
|
eventEmitter.emit('line:jump', lineNumber)
|
||||||
return null
|
}
|
||||||
|
} else {
|
||||||
|
if (lineNumber !== -1) {
|
||||||
|
eventEmitter.emit('editor:focus')
|
||||||
|
eventEmitter.emit('line:jump', lineNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
|
|
||||||
|
if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp (e) {
|
handleMouseUp (e) {
|
||||||
@@ -298,58 +334,83 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.exportAsDocument('md')
|
this.exportAsDocument('md')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsHtml () {
|
htmlContentFormatter (noteContent, exportTasks, targetDir) {
|
||||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
const {
|
||||||
const {
|
fontFamily,
|
||||||
fontFamily,
|
fontSize,
|
||||||
fontSize,
|
codeBlockFontFamily,
|
||||||
codeBlockFontFamily,
|
lineNumber,
|
||||||
lineNumber,
|
codeBlockTheme,
|
||||||
codeBlockTheme,
|
scrollPastEnd,
|
||||||
scrollPastEnd,
|
theme,
|
||||||
theme,
|
allowCustomCSS,
|
||||||
allowCustomCSS,
|
customCSS,
|
||||||
customCSS
|
RTL
|
||||||
} = this.getStyleParams()
|
} = this.getStyleParams()
|
||||||
|
|
||||||
const inlineStyles = buildStyle(
|
const inlineStyles = buildStyle({
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
codeBlockFontFamily,
|
codeBlockFontFamily,
|
||||||
lineNumber,
|
lineNumber,
|
||||||
scrollPastEnd,
|
scrollPastEnd,
|
||||||
theme,
|
theme,
|
||||||
allowCustomCSS,
|
allowCustomCSS,
|
||||||
customCSS
|
customCSS,
|
||||||
)
|
RTL
|
||||||
let body = this.markdown.render(noteContent)
|
})
|
||||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
let body = this.refs.root.contentWindow.document.body.innerHTML
|
||||||
files.forEach(file => {
|
body = attachmentManagement.fixLocalURLS(
|
||||||
if (global.process.platform === 'win32') {
|
body,
|
||||||
file = file.replace('file:///', '')
|
this.props.storagePath
|
||||||
} else {
|
)
|
||||||
file = file.replace('file://', '')
|
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
}
|
files.forEach(file => {
|
||||||
exportTasks.push({
|
if (global.process.platform === 'win32') {
|
||||||
src: file,
|
file = file.replace('file:///', '')
|
||||||
dst: 'css'
|
} else {
|
||||||
|
file = file.replace('file://', '')
|
||||||
|
}
|
||||||
|
exportTasks.push({
|
||||||
|
src: file,
|
||||||
|
dst: 'css'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let styles = ''
|
||||||
|
files.forEach(file => {
|
||||||
|
styles += `<link rel="stylesheet" href="../css/${path.basename(file)}">`
|
||||||
|
})
|
||||||
|
|
||||||
|
return `<html>
|
||||||
|
<head>
|
||||||
|
<base href="file://${targetDir}/">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||||
|
<style id="style">${inlineStyles}</style>
|
||||||
|
${styles}
|
||||||
|
</head>
|
||||||
|
<body>${body}</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaveAsHtml () {
|
||||||
|
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir)))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaveAsPdf () {
|
||||||
|
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
|
||||||
|
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false, javascript: false}})
|
||||||
|
printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir))
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
printout.webContents.on('did-finish-load', () => {
|
||||||
|
printout.webContents.printToPDF({}, (err, data) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
else resolve(data)
|
||||||
|
printout.destroy()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
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>`
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +434,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
.then(res => {
|
.then(res => {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: `Exported to ${filename}`
|
message: `Exported to ${filename}`,
|
||||||
|
buttons: [i18n.__('Ok')]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -480,9 +542,14 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
'scroll',
|
'scroll',
|
||||||
this.scrollHandler
|
this.scrollHandler
|
||||||
)
|
)
|
||||||
|
this.refs.root.contentWindow.addEventListener(
|
||||||
|
'resize',
|
||||||
|
this.resizeHandler
|
||||||
|
)
|
||||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||||
|
eventEmitter.on('export:save-pdf', this.saveAsPdfHandler)
|
||||||
eventEmitter.on('print', this.printHandler)
|
eventEmitter.on('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,23 +584,31 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
'scroll',
|
'scroll',
|
||||||
this.scrollHandler
|
this.scrollHandler
|
||||||
)
|
)
|
||||||
|
this.refs.root.contentWindow.removeEventListener(
|
||||||
|
'resize',
|
||||||
|
this.resizeHandler
|
||||||
|
)
|
||||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||||
|
eventEmitter.off('export:save-pdf', this.saveAsPdfHandler)
|
||||||
eventEmitter.off('print', this.printHandler)
|
eventEmitter.off('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
// actual rewriteIframe function should be called only once
|
||||||
|
let needsRewriteIframe = false
|
||||||
|
if (prevProps.value !== this.props.value) needsRewriteIframe = true
|
||||||
if (
|
if (
|
||||||
prevProps.smartQuotes !== this.props.smartQuotes ||
|
prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||||
prevProps.sanitize !== this.props.sanitize ||
|
prevProps.sanitize !== this.props.sanitize ||
|
||||||
|
prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel ||
|
||||||
prevProps.smartArrows !== this.props.smartArrows ||
|
prevProps.smartArrows !== this.props.smartArrows ||
|
||||||
prevProps.breaks !== this.props.breaks ||
|
prevProps.breaks !== this.props.breaks ||
|
||||||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
|
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
|
||||||
) {
|
) {
|
||||||
this.initMarkdown()
|
this.initMarkdown()
|
||||||
this.rewriteIframe()
|
needsRewriteIframe = true
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
prevProps.fontFamily !== this.props.fontFamily ||
|
prevProps.fontFamily !== this.props.fontFamily ||
|
||||||
@@ -545,11 +620,21 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
prevProps.theme !== this.props.theme ||
|
prevProps.theme !== this.props.theme ||
|
||||||
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
|
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
|
||||||
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
||||||
prevProps.customCSS !== this.props.customCSS
|
prevProps.customCSS !== this.props.customCSS ||
|
||||||
|
prevProps.RTL !== this.props.RTL
|
||||||
) {
|
) {
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
|
needsRewriteIframe = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsRewriteIframe) {
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should scroll to top after selecting another note
|
||||||
|
if (prevProps.noteKey !== this.props.noteKey) {
|
||||||
|
this.scrollTo(0, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStyleParams () {
|
getStyleParams () {
|
||||||
@@ -560,7 +645,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
scrollPastEnd,
|
scrollPastEnd,
|
||||||
theme,
|
theme,
|
||||||
allowCustomCSS,
|
allowCustomCSS,
|
||||||
customCSS
|
customCSS,
|
||||||
|
RTL
|
||||||
} = this.props
|
} = 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
|
||||||
@@ -586,7 +672,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
scrollPastEnd,
|
scrollPastEnd,
|
||||||
theme,
|
theme,
|
||||||
allowCustomCSS,
|
allowCustomCSS,
|
||||||
customCSS
|
customCSS,
|
||||||
|
RTL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,13 +687,14 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
scrollPastEnd,
|
scrollPastEnd,
|
||||||
theme,
|
theme,
|
||||||
allowCustomCSS,
|
allowCustomCSS,
|
||||||
customCSS
|
customCSS,
|
||||||
|
RTL
|
||||||
} = this.getStyleParams()
|
} = this.getStyleParams()
|
||||||
|
|
||||||
this.getWindow().document.getElementById(
|
this.getWindow().document.getElementById(
|
||||||
'codeTheme'
|
'codeTheme'
|
||||||
).href = this.GetCodeThemeLink(codeBlockTheme)
|
).href = this.getCodeThemeLink(codeBlockTheme)
|
||||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle({
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
codeBlockFontFamily,
|
codeBlockFontFamily,
|
||||||
@@ -614,18 +702,17 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
scrollPastEnd,
|
scrollPastEnd,
|
||||||
theme,
|
theme,
|
||||||
allowCustomCSS,
|
allowCustomCSS,
|
||||||
customCSS
|
customCSS,
|
||||||
)
|
RTL
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
GetCodeThemeLink (theme) {
|
getCodeThemeLink (name) {
|
||||||
theme = consts.THEMES.some(_theme => _theme === theme) &&
|
const theme = consts.THEMES.find(theme => theme.name === name)
|
||||||
theme !== 'default'
|
|
||||||
? theme
|
return theme != null
|
||||||
: 'elegant'
|
? theme.path
|
||||||
return theme.startsWith('solarized')
|
: `${appPath}/node_modules/codemirror/theme/elegant.css`
|
||||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
|
||||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rewriteIframe () {
|
rewriteIframe () {
|
||||||
@@ -651,7 +738,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
showCopyNotification,
|
showCopyNotification,
|
||||||
storagePath,
|
storagePath,
|
||||||
noteKey,
|
noteKey,
|
||||||
sanitize
|
sanitize,
|
||||||
|
mermaidHTMLLabel
|
||||||
} = this.props
|
} = this.props
|
||||||
let { value, codeBlockTheme } = this.props
|
let { value, codeBlockTheme } = this.props
|
||||||
|
|
||||||
@@ -683,9 +771,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
|
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
|
||||||
? codeBlockTheme
|
|
||||||
: 'default'
|
const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default'
|
||||||
|
|
||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
||||||
@@ -698,6 +786,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
copyIcon.innerHTML =
|
copyIcon.innerHTML =
|
||||||
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||||
copyIcon.onclick = e => {
|
copyIcon.onclick = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
copy(content)
|
copy(content)
|
||||||
if (showCopyNotification) {
|
if (showCopyNotification) {
|
||||||
this.notify('Saved to Clipboard!', {
|
this.notify('Saved to Clipboard!', {
|
||||||
@@ -706,14 +796,11 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
el.parentNode.appendChild(copyIcon)
|
el.parentNode.appendChild(copyIcon)
|
||||||
el.innerHTML = ''
|
el.innerHTML = ''
|
||||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
el.parentNode.className += ` ${codeBlockThemeClassName}`
|
||||||
const [refThema, color] = codeBlockTheme.split(' ')
|
|
||||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
|
||||||
} else {
|
|
||||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
|
||||||
}
|
|
||||||
CodeMirror.runMode(content, syntax.mime, el, {
|
CodeMirror.runMode(content, syntax.mime, el, {
|
||||||
tabSize: indentSize
|
tabSize: indentSize
|
||||||
})
|
})
|
||||||
@@ -784,6 +871,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
canvas.height = height.value + 'vh'
|
canvas.height = height.value + 'vh'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const chart = new Chart(canvas, chartConfig)
|
const chart = new Chart(canvas, chartConfig)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
el.className = 'chart-error'
|
el.className = 'chart-error'
|
||||||
@@ -794,7 +882,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
||||||
el => {
|
el => {
|
||||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
|
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme, mermaidHTMLLabel)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -828,80 +916,111 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
|
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
|
||||||
const rect = markdownPreviewIframe.getBoundingClientRect()
|
const rect = markdownPreviewIframe.getBoundingClientRect()
|
||||||
|
const config = { attributes: true, subtree: true }
|
||||||
|
const imgObserver = new MutationObserver((mutationList) => {
|
||||||
|
for (const mu of mutationList) {
|
||||||
|
if (mu.target.className === 'carouselContent-enter-done') {
|
||||||
|
this.setImgOnClickEventHelper(mu.target, rect)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
|
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
|
||||||
for (const img of imgList) {
|
for (const img of imgList) {
|
||||||
img.onclick = () => {
|
const parentEl = img.parentElement
|
||||||
const widthMagnification = document.body.clientWidth / img.width
|
this.setImgOnClickEventHelper(img, rect)
|
||||||
const heightMagnification = document.body.clientHeight / img.height
|
imgObserver.observe(parentEl, config)
|
||||||
const baseOnWidth = widthMagnification < heightMagnification
|
}
|
||||||
const magnification = baseOnWidth ? widthMagnification : heightMagnification
|
|
||||||
|
|
||||||
const zoomImgWidth = img.width * magnification
|
const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('a')
|
||||||
const zoomImgHeight = img.height * magnification
|
for (const a of aList) {
|
||||||
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
|
a.removeEventListener('click', this.linkClickHandler)
|
||||||
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
|
a.addEventListener('click', this.linkClickHandler)
|
||||||
const originalImgTop = img.y + rect.top
|
}
|
||||||
const originalImgLeft = img.x + rect.left
|
}
|
||||||
const originalImgRect = {
|
|
||||||
top: `${originalImgTop}px`,
|
|
||||||
left: `${originalImgLeft}px`,
|
|
||||||
width: `${img.width}px`,
|
|
||||||
height: `${img.height}px`
|
|
||||||
}
|
|
||||||
const zoomInImgRect = {
|
|
||||||
top: `${baseOnWidth ? zoomImgTop : 0}px`,
|
|
||||||
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
|
|
||||||
width: `${zoomImgWidth}px`,
|
|
||||||
height: `${zoomImgHeight}px`
|
|
||||||
}
|
|
||||||
const animationSpeed = 300
|
|
||||||
|
|
||||||
const zoomImg = document.createElement('img')
|
setImgOnClickEventHelper (img, rect) {
|
||||||
zoomImg.src = img.src
|
img.onclick = () => {
|
||||||
|
const widthMagnification = document.body.clientWidth / img.width
|
||||||
|
const heightMagnification = document.body.clientHeight / img.height
|
||||||
|
const baseOnWidth = widthMagnification < heightMagnification
|
||||||
|
const magnification = baseOnWidth ? widthMagnification : heightMagnification
|
||||||
|
|
||||||
|
const zoomImgWidth = img.width * magnification
|
||||||
|
const zoomImgHeight = img.height * magnification
|
||||||
|
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
|
||||||
|
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
|
||||||
|
const originalImgTop = img.y + rect.top
|
||||||
|
const originalImgLeft = img.x + rect.left
|
||||||
|
const originalImgRect = {
|
||||||
|
top: `${originalImgTop}px`,
|
||||||
|
left: `${originalImgLeft}px`,
|
||||||
|
width: `${img.width}px`,
|
||||||
|
height: `${img.height}px`
|
||||||
|
}
|
||||||
|
const zoomInImgRect = {
|
||||||
|
top: `${baseOnWidth ? zoomImgTop : 0}px`,
|
||||||
|
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
|
||||||
|
width: `${zoomImgWidth}px`,
|
||||||
|
height: `${zoomImgHeight}px`
|
||||||
|
}
|
||||||
|
const animationSpeed = 300
|
||||||
|
|
||||||
|
const zoomImg = document.createElement('img')
|
||||||
|
zoomImg.src = img.src
|
||||||
|
zoomImg.style = `
|
||||||
|
position: absolute;
|
||||||
|
top: ${baseOnWidth ? zoomImgTop : 0}px;
|
||||||
|
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
|
||||||
|
width: ${zoomImgWidth};
|
||||||
|
height: ${zoomImgHeight}px;
|
||||||
|
`
|
||||||
|
zoomImg.animate([
|
||||||
|
originalImgRect,
|
||||||
|
zoomInImgRect
|
||||||
|
], animationSpeed)
|
||||||
|
|
||||||
|
const overlay = document.createElement('div')
|
||||||
|
overlay.style = `
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
cursor: zoom-out;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: ${document.body.clientHeight}px;
|
||||||
|
z-index: 100;
|
||||||
|
`
|
||||||
|
overlay.onclick = () => {
|
||||||
zoomImg.style = `
|
zoomImg.style = `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${baseOnWidth ? zoomImgTop : 0}px;
|
top: ${originalImgTop}px;
|
||||||
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
|
left: ${originalImgLeft}px;
|
||||||
width: ${zoomImgWidth};
|
width: ${img.width}px;
|
||||||
height: ${zoomImgHeight}px;
|
height: ${img.height}px;
|
||||||
`
|
`
|
||||||
zoomImg.animate([
|
const zoomOutImgAnimation = zoomImg.animate([
|
||||||
originalImgRect,
|
zoomInImgRect,
|
||||||
zoomInImgRect
|
originalImgRect
|
||||||
], animationSpeed)
|
], animationSpeed)
|
||||||
|
zoomOutImgAnimation.onfinish = () => overlay.remove()
|
||||||
const overlay = document.createElement('div')
|
|
||||||
overlay.style = `
|
|
||||||
background-color: rgba(0,0,0,0.5);
|
|
||||||
cursor: zoom-out;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: ${document.body.clientHeight}px;
|
|
||||||
z-index: 100;
|
|
||||||
`
|
|
||||||
overlay.onclick = () => {
|
|
||||||
zoomImg.style = `
|
|
||||||
position: absolute;
|
|
||||||
top: ${originalImgTop}px;
|
|
||||||
left: ${originalImgLeft}px;
|
|
||||||
width: ${img.width}px;
|
|
||||||
height: ${img.height}px;
|
|
||||||
`
|
|
||||||
const zoomOutImgAnimation = zoomImg.animate([
|
|
||||||
zoomInImgRect,
|
|
||||||
originalImgRect
|
|
||||||
], animationSpeed)
|
|
||||||
zoomOutImgAnimation.onfinish = () => overlay.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
overlay.appendChild(zoomImg)
|
|
||||||
document.body.appendChild(overlay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
overlay.appendChild(zoomImg)
|
||||||
|
document.body.appendChild(overlay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleResize () {
|
||||||
|
_.forEach(
|
||||||
|
this.refs.root.contentWindow.document.querySelectorAll('svg[ratio]'),
|
||||||
|
el => {
|
||||||
|
el.setAttribute('height', el.clientWidth / el.getAttribute('ratio'))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
this.refs.root.focus()
|
this.refs.root.focus()
|
||||||
}
|
}
|
||||||
@@ -910,7 +1029,11 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
return this.refs.root.contentWindow
|
return this.refs.root.contentWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollTo (targetRow) {
|
/**
|
||||||
|
* @public
|
||||||
|
* @param {Number} targetRow
|
||||||
|
*/
|
||||||
|
scrollToRow (targetRow) {
|
||||||
const blocks = this.getWindow().document.querySelectorAll(
|
const blocks = this.getWindow().document.querySelectorAll(
|
||||||
'body>[data-line]'
|
'body>[data-line]'
|
||||||
)
|
)
|
||||||
@@ -920,12 +1043,21 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
const row = parseInt(block.getAttribute('data-line'))
|
const row = parseInt(block.getAttribute('data-line'))
|
||||||
if (row > targetRow || index === blocks.length - 1) {
|
if (row > targetRow || index === blocks.length - 1) {
|
||||||
block = blocks[index - 1]
|
block = blocks[index - 1]
|
||||||
block != null && this.getWindow().scrollTo(0, block.offsetTop)
|
block != null && this.scrollTo(0, block.offsetTop)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `document.body.scrollTo`
|
||||||
|
* @param {Number} x
|
||||||
|
* @param {Number} y
|
||||||
|
*/
|
||||||
|
scrollTo (x, y) {
|
||||||
|
this.getWindow().document.body.scrollTo(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
preventImageDroppedHandler (e) {
|
preventImageDroppedHandler (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -946,20 +1078,32 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
const href = e.target.href
|
const rawHref = e.target.getAttribute('href')
|
||||||
const linkHash = href.split('/').pop()
|
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
|
||||||
|
|
||||||
const regexNoteInternalLink = /main.html#(.+)/
|
const parser = document.createElement('a')
|
||||||
if (regexNoteInternalLink.test(linkHash)) {
|
parser.href = rawHref
|
||||||
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
|
const isStartWithHash = rawHref[0] === '#'
|
||||||
const targetElement = this.refs.root.contentWindow.document.getElementById(
|
const { href, hash } = parser
|
||||||
targetId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (targetElement != null) {
|
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
|
||||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
|
||||||
|
const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html
|
||||||
|
const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`)
|
||||||
|
if (isStartWithHash || regexNoteInternalLink.test(rawHref)) {
|
||||||
|
const posOfHash = linkHash.indexOf('#')
|
||||||
|
if (posOfHash > -1) {
|
||||||
|
const extractedId = linkHash.slice(posOfHash + 1)
|
||||||
|
const targetId = mdurl.encode(extractedId)
|
||||||
|
const targetElement = this.getWindow().document.getElementById(
|
||||||
|
targetId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (targetElement != null) {
|
||||||
|
this.scrollTo(0, targetElement.offsetTop)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this will match the new uuid v4 hash and the old hash
|
// this will match the new uuid v4 hash and the old hash
|
||||||
|
|||||||
@@ -78,24 +78,25 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const idMatch = /checkbox-([0-9]+)/
|
const idMatch = /checkbox-([0-9]+)/
|
||||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
|
||||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
|
||||||
const checkReplace = /\[x\]/i
|
const checkReplace = /\[x]/i
|
||||||
const uncheckReplace = /\[ \]/
|
const uncheckReplace = /\[ ]/
|
||||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
const lines = this.refs.code.value
|
const lines = this.refs.code.value
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|
||||||
const targetLine = lines[lineIndex]
|
const targetLine = lines[lineIndex]
|
||||||
|
let newLine = targetLine
|
||||||
|
|
||||||
if (targetLine.match(checkedMatch)) {
|
if (targetLine.match(checkedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
newLine = targetLine.replace(checkReplace, '[ ]')
|
||||||
}
|
}
|
||||||
if (targetLine.match(uncheckedMatch)) {
|
if (targetLine.match(uncheckedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
newLine = targetLine.replace(uncheckReplace, '[x]')
|
||||||
}
|
}
|
||||||
this.refs.code.setValue(lines.join('\n'))
|
this.refs.code.setLineContent(lineIndex, newLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {config, value, storageKey, noteKey, linesHighlighted} = this.props
|
const {config, value, storageKey, noteKey, linesHighlighted, RTL} = 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
|
||||||
@@ -150,7 +151,6 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
onMouseMove={e => this.handleMouseMove(e)}
|
onMouseMove={e => this.handleMouseMove(e)}
|
||||||
onMouseUp={e => this.handleMouseUp(e)}>
|
onMouseUp={e => this.handleMouseUp(e)}>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
styleName='codeEditor'
|
|
||||||
ref='code'
|
ref='code'
|
||||||
width={this.state.codeEditorWidthInPercent + '%'}
|
width={this.state.codeEditorWidthInPercent + '%'}
|
||||||
mode='Boost Flavored Markdown'
|
mode='Boost Flavored Markdown'
|
||||||
@@ -160,6 +160,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
fontFamily={config.editor.fontFamily}
|
fontFamily={config.editor.fontFamily}
|
||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
lineWrapping
|
||||||
matchingPairs={config.editor.matchingPairs}
|
matchingPairs={config.editor.matchingPairs}
|
||||||
matchingTriples={config.editor.matchingTriples}
|
matchingTriples={config.editor.matchingTriples}
|
||||||
explodingPairs={config.editor.explodingPairs}
|
explodingPairs={config.editor.explodingPairs}
|
||||||
@@ -179,13 +180,16 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
enableSmartPaste={config.editor.enableSmartPaste}
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
hotkey={config.hotkey}
|
hotkey={config.hotkey}
|
||||||
switchPreview={config.editor.switchPreview}
|
switchPreview={config.editor.switchPreview}
|
||||||
|
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||||
|
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||||
|
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||||
|
RTL={RTL}
|
||||||
/>
|
/>
|
||||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||||
<div styleName='slider-hitbox' />
|
<div styleName='slider-hitbox' />
|
||||||
</div>
|
</div>
|
||||||
<MarkdownPreview
|
<MarkdownPreview
|
||||||
style={previewStyle}
|
style={previewStyle}
|
||||||
styleName='preview'
|
|
||||||
theme={config.ui.theme}
|
theme={config.ui.theme}
|
||||||
keyMap={config.editor.keyMap}
|
keyMap={config.editor.keyMap}
|
||||||
fontSize={config.preview.fontSize}
|
fontSize={config.preview.fontSize}
|
||||||
@@ -198,6 +202,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
smartArrows={config.preview.smartArrows}
|
smartArrows={config.preview.smartArrows}
|
||||||
breaks={config.preview.breaks}
|
breaks={config.preview.breaks}
|
||||||
sanitize={config.preview.sanitize}
|
sanitize={config.preview.sanitize}
|
||||||
|
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
tabInde='0'
|
tabInde='0'
|
||||||
value={value}
|
value={value}
|
||||||
@@ -209,6 +214,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
customCSS={config.preview.customCSS}
|
customCSS={config.preview.customCSS}
|
||||||
allowCustomCSS={config.preview.allowCustomCSS}
|
allowCustomCSS={config.preview.allowCustomCSS}
|
||||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||||
|
RTL={RTL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,30 @@
|
|||||||
top -2px
|
top -2px
|
||||||
width 0
|
width 0
|
||||||
z-index 0
|
z-index 0
|
||||||
|
border-left 1px solid $ui-borderColor
|
||||||
.slider-hitbox
|
.slider-hitbox
|
||||||
absolute top bottom left right
|
absolute top bottom left right
|
||||||
width 7px
|
width 7px
|
||||||
left -3px
|
left -3px
|
||||||
z-index 10
|
z-index 10
|
||||||
cursor col-resize
|
cursor col-resize
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
.slider
|
||||||
|
border-left 1px solid $ui-dark-borderColor
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
.slider
|
||||||
|
border-left 1px solid $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.root
|
||||||
|
.slider
|
||||||
|
border-left 1px solid $ui-monokai-borderColor
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
.slider
|
||||||
|
border-left 1px solid $ui-dracula-borderColor
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const ModalEscButton = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<button styleName='escButton' onClick={handleEscButtonClick}>
|
<button styleName='escButton' onClick={handleEscButtonClick}>
|
||||||
<div styleName='esc-mark'>×</div>
|
<div styleName='esc-mark'>×</div>
|
||||||
<div styleName='esc-text'>esc</div>
|
<div>esc</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const NavToggleButton = ({isFolded, handleToggleButtonClick}) => (
|
|||||||
onClick={(e) => handleToggleButtonClick(e)}
|
onClick={(e) => handleToggleButtonClick(e)}
|
||||||
>
|
>
|
||||||
{isFolded
|
{isFolded
|
||||||
? <i className='fa fa-angle-double-right' />
|
? <i className='fa fa-angle-double-right fa-2x' />
|
||||||
: <i className='fa fa-angle-double-left' />
|
: <i className='fa fa-angle-double-left fa-2x' />
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
border-radius 16.5px
|
border-radius 16.5px
|
||||||
height 34px
|
height 34px
|
||||||
width 34px
|
width 34px
|
||||||
line-height 32px
|
line-height 100%
|
||||||
padding 0
|
padding 0
|
||||||
&:hover
|
&:hover
|
||||||
border: 1px solid #1EC38B;
|
border: 1px solid #1EC38B;
|
||||||
@@ -17,10 +17,16 @@
|
|||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
navWhiteButtonColor()
|
navWhiteButtonColor()
|
||||||
|
|
||||||
body[data-theme="dark"]
|
apply-theme(theme)
|
||||||
.navToggle
|
body[data-theme={theme}]
|
||||||
&:hover
|
.navToggle:hover
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%)
|
||||||
|
border 1px solid get-theme-var(theme, 'button--active-backgroundColor')
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
color $ui-dark-text-color
|
color get-theme-var(theme, 'text-color')
|
||||||
|
|
||||||
|
for theme in 'dark' 'dracula' 'solarized-dark'
|
||||||
|
apply-theme(theme)
|
||||||
|
|
||||||
|
for theme in $themes
|
||||||
|
apply-theme(theme)
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { isArray } from 'lodash'
|
import { isArray, sortBy } from 'lodash'
|
||||||
import invertColor from 'invert-color'
|
import invertColor from 'invert-color'
|
||||||
|
import Emoji from 'react-emoji-render'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
||||||
import styles from './NoteItem.styl'
|
import styles from './NoteItem.styl'
|
||||||
@@ -43,7 +44,7 @@ const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showTagsAlphabetically) {
|
if (showTagsAlphabetically) {
|
||||||
return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
return sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||||
} else {
|
} else {
|
||||||
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||||
}
|
}
|
||||||
@@ -87,7 +88,7 @@ const NoteItem = ({
|
|||||||
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
|
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
|
||||||
<div styleName='item-title'>
|
<div styleName='item-title'>
|
||||||
{note.title.trim().length > 0
|
{note.title.trim().length > 0
|
||||||
? note.title
|
? <Emoji text={note.title} />
|
||||||
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
|
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div styleName='item-middle'>
|
<div styleName='item-middle'>
|
||||||
@@ -148,15 +149,14 @@ NoteItem.propTypes = {
|
|||||||
tags: PropTypes.array,
|
tags: PropTypes.array,
|
||||||
isStarred: PropTypes.bool.isRequired,
|
isStarred: PropTypes.bool.isRequired,
|
||||||
isTrashed: PropTypes.bool.isRequired,
|
isTrashed: PropTypes.bool.isRequired,
|
||||||
blog: {
|
blog: PropTypes.shape({
|
||||||
blogLink: PropTypes.string,
|
blogLink: PropTypes.string,
|
||||||
blogId: PropTypes.number
|
blogId: PropTypes.number
|
||||||
}
|
})
|
||||||
}),
|
}),
|
||||||
handleNoteClick: PropTypes.func.isRequired,
|
handleNoteClick: PropTypes.func.isRequired,
|
||||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||||
handleDragStart: PropTypes.func.isRequired,
|
handleDragStart: PropTypes.func.isRequired
|
||||||
handleDragEnd: PropTypes.func.isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(NoteItem, styles)
|
export default CSSModules(NoteItem, styles)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ SideNavFilter.propTypes = {
|
|||||||
isStarredActive: PropTypes.bool.isRequired,
|
isStarredActive: PropTypes.bool.isRequired,
|
||||||
isTrashedActive: PropTypes.bool.isRequired,
|
isTrashedActive: PropTypes.bool.isRequired,
|
||||||
handleStarredButtonClick: PropTypes.func.isRequired,
|
handleStarredButtonClick: PropTypes.func.isRequired,
|
||||||
handleTrashdButtonClick: PropTypes.func.isRequired
|
handleTrashedButtonClick: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(SideNavFilter, styles)
|
export default CSSModules(SideNavFilter, styles)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class SnippetTab extends React.Component {
|
|||||||
>
|
>
|
||||||
{snippet.name.trim().length > 0
|
{snippet.name.trim().length > 0
|
||||||
? snippet.name
|
? snippet.name
|
||||||
: <span styleName='button-unnamed'>
|
: <span>
|
||||||
{i18n.__('Unnamed')}
|
{i18n.__('Unnamed')}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ const TodoProcess = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
TodoProcess.propTypes = {
|
TodoProcess.propTypes = {
|
||||||
todoStatus: {
|
todoStatus: PropTypes.exact({
|
||||||
total: PropTypes.number.isRequired,
|
total: PropTypes.number.isRequired,
|
||||||
completed: PropTypes.number.isRequired
|
completed: PropTypes.number.isRequired
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(TodoProcess, styles)
|
export default CSSModules(TodoProcess, styles)
|
||||||
|
|||||||
@@ -363,7 +363,10 @@ admonition_types = {
|
|||||||
danger: {color: #c2185b, icon: "block"},
|
danger: {color: #c2185b, icon: "block"},
|
||||||
caution: {color: #ffa726, icon: "warning"},
|
caution: {color: #ffa726, icon: "warning"},
|
||||||
error: {color: #d32f2f, icon: "error_outline"},
|
error: {color: #d32f2f, icon: "error_outline"},
|
||||||
attention: {color: #455a64, icon: "priority_high"}
|
question: {color: #64dd17, icon: "help_outline"},
|
||||||
|
quote: {color: #9e9e9e, icon: "format_quote"},
|
||||||
|
abstract: {color: #00b0ff, icon: "subject"},
|
||||||
|
attention: {color: #455a64, icon: "priority_high"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, val in admonition_types
|
for name, val in admonition_types
|
||||||
@@ -424,6 +427,9 @@ pre.fence
|
|||||||
canvas, svg
|
canvas, svg
|
||||||
max-width 100% !important
|
max-width 100% !important
|
||||||
|
|
||||||
|
svg[ratio]
|
||||||
|
width 100%
|
||||||
|
|
||||||
.gallery
|
.gallery
|
||||||
width 100%
|
width 100%
|
||||||
height 50vh
|
height 50vh
|
||||||
@@ -444,6 +450,44 @@ pre.fence
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
background-color $ui-tag-backgroundColor
|
background-color $ui-tag-backgroundColor
|
||||||
|
|
||||||
|
.markdownIt-TOC-wrapper
|
||||||
|
list-style none
|
||||||
|
position fixed
|
||||||
|
right 0
|
||||||
|
top 0
|
||||||
|
margin-left 15px
|
||||||
|
z-index 1000
|
||||||
|
transition transform .2s ease-in-out
|
||||||
|
transform translateX(100%)
|
||||||
|
|
||||||
|
.markdownIt-TOC
|
||||||
|
display block
|
||||||
|
max-height 90vh
|
||||||
|
overflow-y auto
|
||||||
|
padding 25px
|
||||||
|
padding-left 38px
|
||||||
|
|
||||||
|
&,
|
||||||
|
&:before
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
color: $ui-dark-text-color
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform translateX(-15px)
|
||||||
|
|
||||||
|
&:before
|
||||||
|
content 'TOC'
|
||||||
|
position absolute
|
||||||
|
width 60px
|
||||||
|
height 30px
|
||||||
|
top 60px
|
||||||
|
left -29px
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
transform-origin top left
|
||||||
|
transform rotate(-90deg)
|
||||||
|
|
||||||
themeDarkBackground = darken(#21252B, 10%)
|
themeDarkBackground = darken(#21252B, 10%)
|
||||||
themeDarkText = #f9f9f9
|
themeDarkText = #f9f9f9
|
||||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||||
@@ -511,6 +555,12 @@ body[data-theme="dark"]
|
|||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
background-color $ui-dark-tag-backgroundColor
|
background-color $ui-dark-tag-backgroundColor
|
||||||
|
|
||||||
|
.markdownIt-TOC-wrapper
|
||||||
|
&,
|
||||||
|
&:before
|
||||||
|
background-color darken(themeDarkBackground, 5%)
|
||||||
|
color themeDarkText
|
||||||
|
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
body[data-theme={theme}]
|
body[data-theme={theme}]
|
||||||
color get-theme-var(theme, 'text-color')
|
color get-theme-var(theme, 'text-color')
|
||||||
@@ -554,8 +604,14 @@ apply-theme(theme)
|
|||||||
color get-theme-var(theme, 'button--active-color')
|
color get-theme-var(theme, 'button--active-color')
|
||||||
background-color get-theme-var(theme, 'button-backgroundColor')
|
background-color get-theme-var(theme, 'button-backgroundColor')
|
||||||
|
|
||||||
|
.markdownIt-TOC-wrapper
|
||||||
|
&,
|
||||||
|
&:before
|
||||||
|
background-color darken(get-theme-var(theme, 'noteDetail-backgroundColor'), 15%)
|
||||||
|
color themeDarkText
|
||||||
|
|
||||||
for theme in 'solarized-dark' 'dracula'
|
for theme in 'solarized-dark' 'dracula'
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|
||||||
for theme in $themes
|
for theme in $themes
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ function getId () {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
function render (element, content, theme) {
|
function render (element, content, theme, enableHTMLLabel) {
|
||||||
try {
|
try {
|
||||||
const height = element.attributes.getNamedItem('data-height')
|
const height = element.attributes.getNamedItem('data-height')
|
||||||
|
const isPredefined = height && height.value !== 'undefined'
|
||||||
|
|
||||||
if (height && height.value !== 'undefined') {
|
if (isPredefined) {
|
||||||
element.style.height = height.value + 'vh'
|
element.style.height = height.value + 'vh'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,11 +34,33 @@ function render (element, content, theme) {
|
|||||||
mermaidAPI.initialize({
|
mermaidAPI.initialize({
|
||||||
theme: isDarkTheme ? 'dark' : 'default',
|
theme: isDarkTheme ? 'dark' : 'default',
|
||||||
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
||||||
useMaxWidth: false
|
flowchart: {
|
||||||
|
htmlLabels: enableHTMLLabel
|
||||||
|
},
|
||||||
|
gantt: {
|
||||||
|
useWidth: element.clientWidth
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mermaidAPI.render(getId(), content, (svgGraph) => {
|
mermaidAPI.render(getId(), content, (svgGraph) => {
|
||||||
element.innerHTML = svgGraph
|
element.innerHTML = svgGraph
|
||||||
|
|
||||||
|
if (!isPredefined) {
|
||||||
|
const el = element.firstChild
|
||||||
|
const viewBox = el.getAttribute('viewBox').split(' ')
|
||||||
|
|
||||||
|
let ratio = viewBox[2] / viewBox[3]
|
||||||
|
|
||||||
|
if (el.style.maxWidth) {
|
||||||
|
const maxWidth = parseFloat(el.style.maxWidth)
|
||||||
|
|
||||||
|
ratio *= el.parentNode.clientWidth / maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
el.setAttribute('ratio', ratio)
|
||||||
|
el.setAttribute('height', el.parentNode.clientWidth / ratio)
|
||||||
|
console.log(el)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
element.className = 'mermaid-error'
|
element.className = 'mermaid-error'
|
||||||
|
|||||||
78
browser/lib/CMLanguageList.js
Normal file
78
browser/lib/CMLanguageList.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
export const languageMaps = {
|
||||||
|
brainfuck: 'Brainfuck',
|
||||||
|
cpp: 'C++',
|
||||||
|
cs: 'C#',
|
||||||
|
clojure: 'Clojure',
|
||||||
|
'clojure-repl': 'ClojureScript',
|
||||||
|
cmake: 'CMake',
|
||||||
|
coffeescript: 'CoffeeScript',
|
||||||
|
crystal: 'Crystal',
|
||||||
|
css: 'CSS',
|
||||||
|
d: 'D',
|
||||||
|
dart: 'Dart',
|
||||||
|
delphi: 'Pascal',
|
||||||
|
diff: 'Diff',
|
||||||
|
django: 'Django',
|
||||||
|
dockerfile: 'Dockerfile',
|
||||||
|
ebnf: 'EBNF',
|
||||||
|
elm: 'Elm',
|
||||||
|
erlang: 'Erlang',
|
||||||
|
'erlang-repl': 'Erlang',
|
||||||
|
fortran: 'Fortran',
|
||||||
|
fsharp: 'F#',
|
||||||
|
gherkin: 'Gherkin',
|
||||||
|
go: 'Go',
|
||||||
|
groovy: 'Groovy',
|
||||||
|
haml: 'HAML',
|
||||||
|
haskell: 'Haskell',
|
||||||
|
haxe: 'Haxe',
|
||||||
|
http: 'HTTP',
|
||||||
|
ini: 'toml',
|
||||||
|
java: 'Java',
|
||||||
|
javascript: 'JavaScript',
|
||||||
|
json: 'JSON',
|
||||||
|
julia: 'Julia',
|
||||||
|
kotlin: 'Kotlin',
|
||||||
|
less: 'LESS',
|
||||||
|
livescript: 'LiveScript',
|
||||||
|
lua: 'Lua',
|
||||||
|
markdown: 'Markdown',
|
||||||
|
mathematica: 'Mathematica',
|
||||||
|
nginx: 'Nginx',
|
||||||
|
nsis: 'NSIS',
|
||||||
|
objectivec: 'Objective-C',
|
||||||
|
ocaml: 'Ocaml',
|
||||||
|
perl: 'Perl',
|
||||||
|
php: 'PHP',
|
||||||
|
powershell: 'PowerShell',
|
||||||
|
properties: 'Properties files',
|
||||||
|
protobuf: 'ProtoBuf',
|
||||||
|
python: 'Python',
|
||||||
|
puppet: 'Puppet',
|
||||||
|
q: 'Q',
|
||||||
|
r: 'R',
|
||||||
|
ruby: 'Ruby',
|
||||||
|
rust: 'Rust',
|
||||||
|
sas: 'SAS',
|
||||||
|
scala: 'Scala',
|
||||||
|
scheme: 'Scheme',
|
||||||
|
scss: 'SCSS',
|
||||||
|
shell: 'Shell',
|
||||||
|
smalltalk: 'Smalltalk',
|
||||||
|
sml: 'SML',
|
||||||
|
sql: 'SQL',
|
||||||
|
stylus: 'Stylus',
|
||||||
|
swift: 'Swift',
|
||||||
|
tcl: 'Tcl',
|
||||||
|
tex: 'LaTex',
|
||||||
|
typescript: 'TypeScript',
|
||||||
|
twig: 'Twig',
|
||||||
|
vbnet: 'VB.NET',
|
||||||
|
vbscript: 'VBScript',
|
||||||
|
verilog: 'Verilog',
|
||||||
|
vhdl: 'VHDL',
|
||||||
|
xml: 'HTML',
|
||||||
|
xquery: 'XQuery',
|
||||||
|
yaml: 'YAML',
|
||||||
|
elixir: 'Elixir'
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import CSSModules from 'react-css-modules'
|
import CSSModules from 'react-css-modules'
|
||||||
|
|
||||||
export default function (component, styles) {
|
export default function (component, styles) {
|
||||||
return CSSModules(component, styles, {errorWhenNotFound: false})
|
return CSSModules(component, styles, {handleNotFoundStyleName: 'log'})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ const languages = [
|
|||||||
name: 'Chinese (zh-TW)',
|
name: 'Chinese (zh-TW)',
|
||||||
locale: 'zh-TW'
|
locale: 'zh-TW'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Czech',
|
||||||
|
locale: 'cs'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Danish',
|
name: 'Danish',
|
||||||
locale: 'da'
|
locale: 'da'
|
||||||
@@ -62,10 +66,12 @@ const languages = [
|
|||||||
{
|
{
|
||||||
name: 'Spanish',
|
name: 'Spanish',
|
||||||
locale: 'es-ES'
|
locale: 'es-ES'
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
name: 'Turkish',
|
name: 'Turkish',
|
||||||
locale: 'tr'
|
locale: 'tr'
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
name: 'Thai',
|
name: 'Thai',
|
||||||
locale: 'th'
|
locale: 'th'
|
||||||
}
|
}
|
||||||
@@ -82,4 +88,3 @@ module.exports = {
|
|||||||
return languages
|
return languages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
91
browser/lib/SnippetManager.js
Normal file
91
browser/lib/SnippetManager.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
import fs from 'fs'
|
||||||
|
import consts from './consts'
|
||||||
|
|
||||||
|
class SnippetManager {
|
||||||
|
constructor () {
|
||||||
|
this.defaultSnippet = [
|
||||||
|
{
|
||||||
|
id: crypto.randomBytes(16).toString('hex'),
|
||||||
|
name: 'Dummy text',
|
||||||
|
prefix: ['lorem', 'ipsum'],
|
||||||
|
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
this.snippets = []
|
||||||
|
this.expandSnippet = this.expandSnippet.bind(this)
|
||||||
|
this.init = this.init.bind(this)
|
||||||
|
this.assignSnippets = this.assignSnippets.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
init () {
|
||||||
|
if (fs.existsSync(consts.SNIPPET_FILE)) {
|
||||||
|
try {
|
||||||
|
this.snippets = JSON.parse(
|
||||||
|
fs.readFileSync(consts.SNIPPET_FILE, { encoding: 'UTF-8' })
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error while parsing snippet file')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fs.writeFileSync(
|
||||||
|
consts.SNIPPET_FILE,
|
||||||
|
JSON.stringify(this.defaultSnippet, null, 4),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
this.snippets = this.defaultSnippet
|
||||||
|
}
|
||||||
|
|
||||||
|
assignSnippets (snippets) {
|
||||||
|
this.snippets = snippets
|
||||||
|
}
|
||||||
|
|
||||||
|
expandSnippet (wordBeforeCursor, cursor, cm) {
|
||||||
|
const templateCursorString = ':{}'
|
||||||
|
for (let i = 0; i < this.snippets.length; i++) {
|
||||||
|
if (this.snippets[i].prefix.indexOf(wordBeforeCursor.text) === -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (this.snippets[i].content.indexOf(templateCursorString) !== -1) {
|
||||||
|
const snippetLines = this.snippets[i].content.split('\n')
|
||||||
|
let cursorLineNumber = 0
|
||||||
|
let cursorLinePosition = 0
|
||||||
|
|
||||||
|
let cursorIndex
|
||||||
|
for (let j = 0; j < snippetLines.length; j++) {
|
||||||
|
cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||||
|
|
||||||
|
if (cursorIndex !== -1) {
|
||||||
|
cursorLineNumber = j
|
||||||
|
cursorLinePosition = cursorIndex
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.replaceRange(
|
||||||
|
this.snippets[i].content.replace(templateCursorString, ''),
|
||||||
|
wordBeforeCursor.range.from,
|
||||||
|
wordBeforeCursor.range.to
|
||||||
|
)
|
||||||
|
cm.setCursor({
|
||||||
|
line: cursor.line + cursorLineNumber,
|
||||||
|
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cm.replaceRange(
|
||||||
|
this.snippets[i].content,
|
||||||
|
wordBeforeCursor.range.from,
|
||||||
|
wordBeforeCursor.range.to
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = new SnippetManager()
|
||||||
|
export default manager
|
||||||
@@ -6,7 +6,7 @@ const { dialog } = remote
|
|||||||
export function confirmDeleteNote (confirmDeletion, permanent) {
|
export function confirmDeleteNote (confirmDeletion, permanent) {
|
||||||
if (confirmDeletion || permanent) {
|
if (confirmDeletion || permanent) {
|
||||||
const alertConfig = {
|
const alertConfig = {
|
||||||
ype: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Confirm note deletion'),
|
message: i18n.__('Confirm note deletion'),
|
||||||
detail: i18n.__('This will permanently remove this note.'),
|
detail: i18n.__('This will permanently remove this note.'),
|
||||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
|
|||||||
@@ -3,14 +3,43 @@ const fs = require('sander')
|
|||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { app } = remote
|
const { app } = remote
|
||||||
|
|
||||||
const themePath = process.env.NODE_ENV === 'production'
|
const CODEMIRROR_THEME_PATH = 'node_modules/codemirror/theme'
|
||||||
? path.join(app.getAppPath(), './node_modules/codemirror/theme')
|
const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme'
|
||||||
: require('path').resolve('./node_modules/codemirror/theme')
|
|
||||||
const themes = fs.readdirSync(themePath)
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
.map((themePath) => {
|
|
||||||
return themePath.substring(0, themePath.lastIndexOf('.'))
|
const paths = [
|
||||||
})
|
isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH),
|
||||||
themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
|
isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH)
|
||||||
|
]
|
||||||
|
|
||||||
|
const themes = paths
|
||||||
|
.map(directory => fs.readdirSync(directory).map(file => {
|
||||||
|
const name = file.substring(0, file.lastIndexOf('.'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
path: path.join(directory, file),
|
||||||
|
className: `cm-s-${name}`
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.reduce((accumulator, value) => accumulator.concat(value), [])
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
|
themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, {
|
||||||
|
name: 'solarized dark',
|
||||||
|
path: path.join(paths[0], 'solarized.css'),
|
||||||
|
className: `cm-s-solarized cm-s-dark`
|
||||||
|
}, {
|
||||||
|
name: 'solarized light',
|
||||||
|
path: path.join(paths[0], 'solarized.css'),
|
||||||
|
className: `cm-s-solarized cm-s-light`
|
||||||
|
})
|
||||||
|
themes.splice(0, 0, {
|
||||||
|
name: 'default',
|
||||||
|
path: path.join(paths[0], 'elegant.css'),
|
||||||
|
className: `cm-s-default`
|
||||||
|
})
|
||||||
|
|
||||||
const snippetFile = process.env.NODE_ENV !== 'test'
|
const snippetFile = process.env.NODE_ENV !== 'test'
|
||||||
? path.join(app.getPath('userData'), 'snippets.json')
|
? path.join(app.getPath('userData'), 'snippets.json')
|
||||||
@@ -35,7 +64,7 @@ const consts = {
|
|||||||
'Dodger Blue',
|
'Dodger Blue',
|
||||||
'Violet Eggplant'
|
'Violet Eggplant'
|
||||||
],
|
],
|
||||||
THEMES: ['default'].concat(themes),
|
THEMES: themes,
|
||||||
SNIPPET_FILE: snippetFile,
|
SNIPPET_FILE: snippetFile,
|
||||||
DEFAULT_EDITOR_FONT_FAMILY: [
|
DEFAULT_EDITOR_FONT_FAMILY: [
|
||||||
'Monaco',
|
'Monaco',
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
const {remote} = require('electron')
|
const {remote} = require('electron')
|
||||||
const {Menu} = remote.require('electron')
|
const {Menu} = remote.require('electron')
|
||||||
|
const {clipboard} = remote.require('electron')
|
||||||
|
const {shell} = remote.require('electron')
|
||||||
const spellcheck = require('./spellcheck')
|
const spellcheck = require('./spellcheck')
|
||||||
|
const uri2path = require('file-uri-to-path')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
|
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
|
||||||
@@ -62,4 +68,57 @@ const buildEditorContextMenu = function (editor, event) {
|
|||||||
return Menu.buildFromTemplate(template)
|
return Menu.buildFromTemplate(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = buildEditorContextMenu
|
/**
|
||||||
|
* Creates the context menu that is shown when there is a right click Markdown preview of a (not-snippet) note.
|
||||||
|
* @param {MarkdownPreview} markdownPreview
|
||||||
|
* @param {MouseEvent} event that has triggered the creation of the context menu
|
||||||
|
* @returns {Electron.Menu} The created electron context menu
|
||||||
|
*/
|
||||||
|
const buildMarkdownPreviewContextMenu = function (markdownPreview, event) {
|
||||||
|
if (markdownPreview == null || event == null || event.pageX == null || event.pageY == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default context menu inclusions
|
||||||
|
const template = [{
|
||||||
|
role: 'copy'
|
||||||
|
}, {
|
||||||
|
role: 'selectall'
|
||||||
|
}]
|
||||||
|
|
||||||
|
if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) {
|
||||||
|
// Link opener for files on the local system pointed to by href
|
||||||
|
const href = event.target.href
|
||||||
|
const isLocalFile = href.startsWith('file:')
|
||||||
|
if (isLocalFile) {
|
||||||
|
const absPath = uri2path(href)
|
||||||
|
try {
|
||||||
|
if (fs.lstatSync(absPath).isFile()) {
|
||||||
|
template.push(
|
||||||
|
{
|
||||||
|
label: i18n.__('Show in explorer'),
|
||||||
|
click: (e) => shell.showItemInFolder(absPath)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error while evaluating if the file is locally available', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add option to context menu to copy url
|
||||||
|
template.push(
|
||||||
|
{
|
||||||
|
label: i18n.__('Copy Url'),
|
||||||
|
click: (e) => clipboard.writeText(href)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Menu.buildFromTemplate(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
{
|
||||||
|
buildEditorContextMenu: buildEditorContextMenu,
|
||||||
|
buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
import 'codemirror-mode-elixir'
|
import 'codemirror-mode-elixir'
|
||||||
|
|
||||||
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
const stylusCodeInfo = CodeMirror.modeInfo.find(info => info.name === 'Stylus')
|
||||||
|
if (stylusCodeInfo == null) {
|
||||||
|
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
||||||
|
} else {
|
||||||
|
stylusCodeInfo.alias = ['styl']
|
||||||
|
}
|
||||||
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})
|
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ export function getTodoStatus (content) {
|
|||||||
let numberOfCompletedTodo = 0
|
let numberOfCompletedTodo = 0
|
||||||
|
|
||||||
splitted.forEach((line) => {
|
splitted.forEach((line) => {
|
||||||
const trimmedLine = line.trim()
|
const trimmedLine = line.trim().replace(/^(>\s*)*/, '')
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
|
if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) {
|
||||||
numberOfTodo++
|
numberOfTodo++
|
||||||
}
|
}
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
|
if (trimmedLine.match(/^[+\-*] \[x] ./i)) {
|
||||||
numberOfCompletedTodo++
|
numberOfCompletedTodo++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const _ = require('lodash')
|
|
||||||
const uuidv4 = require('uuid/v4')
|
const uuidv4 = require('uuid/v4')
|
||||||
|
|
||||||
module.exports = function (uuid) {
|
module.exports = function (uuid) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) {
|
|||||||
options
|
options
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.tokens[tokenIdx].type === '_fence') {
|
if (state.tokens[tokenIdx].type.match(/.*_fence$/)) {
|
||||||
// escapeHtmlCharacters has better performance
|
// escapeHtmlCharacters has better performance
|
||||||
state.tokens[tokenIdx].content = escapeHtmlCharacters(
|
state.tokens[tokenIdx].content = escapeHtmlCharacters(
|
||||||
state.tokens[tokenIdx].content,
|
state.tokens[tokenIdx].content,
|
||||||
@@ -96,6 +96,10 @@ function sanitizeInline (html, options) {
|
|||||||
|
|
||||||
function naughtyHRef (href, options) {
|
function naughtyHRef (href, options) {
|
||||||
// href = href.replace(/[\x00-\x20]+/g, '')
|
// href = href.replace(/[\x00-\x20]+/g, '')
|
||||||
|
if (!href) {
|
||||||
|
// No href
|
||||||
|
return false
|
||||||
|
}
|
||||||
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
|
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
|
||||||
|
|
||||||
const matches = href.match(/^([a-zA-Z]+)\:/)
|
const matches = href.match(/^([a-zA-Z]+)\:/)
|
||||||
|
|||||||
@@ -21,13 +21,15 @@ function uniqueSlug (slug, slugs, opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function linkify (token) {
|
function linkify (token) {
|
||||||
token.content = mdlink(token.content, '#' + token.slug)
|
token.content = mdlink(token.content, `#${decodeURI(token.slug)}`)
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOC_MARKER_START = '<!-- toc -->'
|
const TOC_MARKER_START = '<!-- toc -->'
|
||||||
const TOC_MARKER_END = '<!-- tocstop -->'
|
const TOC_MARKER_END = '<!-- tocstop -->'
|
||||||
|
|
||||||
|
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes care of proper updating given editor with TOC.
|
* Takes care of proper updating given editor with TOC.
|
||||||
* If TOC doesn't exit in the editor, it's inserted at current caret position.
|
* If TOC doesn't exit in the editor, it's inserted at current caret position.
|
||||||
@@ -35,12 +37,6 @@ const TOC_MARKER_END = '<!-- tocstop -->'
|
|||||||
* @param editor CodeMirror editor to be updated with TOC
|
* @param editor CodeMirror editor to be updated with TOC
|
||||||
*/
|
*/
|
||||||
export function generateInEditor (editor) {
|
export function generateInEditor (editor) {
|
||||||
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
|
|
||||||
|
|
||||||
function tocExistsInEditor () {
|
|
||||||
return tocRegex.test(editor.getValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateExistingToc () {
|
function updateExistingToc () {
|
||||||
const toc = generate(editor.getValue())
|
const toc = generate(editor.getValue())
|
||||||
const search = editor.getSearchCursor(tocRegex)
|
const search = editor.getSearchCursor(tocRegex)
|
||||||
@@ -54,13 +50,17 @@ export function generateInEditor (editor) {
|
|||||||
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tocExistsInEditor()) {
|
if (tocExistsInEditor(editor)) {
|
||||||
updateExistingToc()
|
updateExistingToc()
|
||||||
} else {
|
} else {
|
||||||
addTocAtCursorPosition()
|
addTocAtCursorPosition()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function tocExistsInEditor (editor) {
|
||||||
|
return tocRegex.test(editor.getValue())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates MD TOC based on MD document passed as string.
|
* Generates MD TOC based on MD document passed as string.
|
||||||
* @param markdownText MD document
|
* @param markdownText MD document
|
||||||
@@ -94,5 +94,6 @@ function wrapTocWithEol (toc, editor) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
generate,
|
generate,
|
||||||
generateInEditor
|
generateInEditor,
|
||||||
|
tocExistsInEditor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import markdownit from 'markdown-it'
|
|||||||
import sanitize from './markdown-it-sanitize-html'
|
import sanitize from './markdown-it-sanitize-html'
|
||||||
import emoji from 'markdown-it-emoji'
|
import emoji from 'markdown-it-emoji'
|
||||||
import math from '@rokt33r/markdown-it-math'
|
import math from '@rokt33r/markdown-it-math'
|
||||||
|
import mdurl from 'mdurl'
|
||||||
import smartArrows from 'markdown-it-smartarrows'
|
import smartArrows from 'markdown-it-smartarrows'
|
||||||
|
import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import katex from 'katex'
|
import katex from 'katex'
|
||||||
@@ -32,6 +34,7 @@ class Markdown {
|
|||||||
|
|
||||||
const updatedOptions = Object.assign(defaultOptions, options)
|
const updatedOptions = Object.assign(defaultOptions, options)
|
||||||
this.md = markdownit(updatedOptions)
|
this.md = markdownit(updatedOptions)
|
||||||
|
this.md.linkify.set({ fuzzyLink: false })
|
||||||
|
|
||||||
if (updatedOptions.sanitize !== 'NONE') {
|
if (updatedOptions.sanitize !== 'NONE') {
|
||||||
const allowedTags = ['iframe', 'input', 'b',
|
const allowedTags = ['iframe', 'input', 'b',
|
||||||
@@ -121,10 +124,23 @@ class Markdown {
|
|||||||
slugify: require('./slugify')
|
slugify: require('./slugify')
|
||||||
})
|
})
|
||||||
this.md.use(require('markdown-it-kbd'))
|
this.md.use(require('markdown-it-kbd'))
|
||||||
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
|
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error', 'quote', 'abstract', 'question']})
|
||||||
this.md.use(require('markdown-it-abbr'))
|
this.md.use(require('markdown-it-abbr'))
|
||||||
this.md.use(require('markdown-it-sub'))
|
this.md.use(require('markdown-it-sub'))
|
||||||
this.md.use(require('markdown-it-sup'))
|
this.md.use(require('markdown-it-sup'))
|
||||||
|
|
||||||
|
this.md.use(md => {
|
||||||
|
markdownItTocAndAnchor(md, {
|
||||||
|
toc: true,
|
||||||
|
tocPattern: /\[TOC\]/i,
|
||||||
|
anchorLink: false,
|
||||||
|
appendIdToHeading: false
|
||||||
|
})
|
||||||
|
|
||||||
|
md.renderer.rules.toc_open = () => '<div class="markdownIt-TOC-wrapper">'
|
||||||
|
md.renderer.rules.toc_close = () => '</div>'
|
||||||
|
})
|
||||||
|
|
||||||
this.md.use(require('./markdown-it-deflist'))
|
this.md.use(require('./markdown-it-deflist'))
|
||||||
this.md.use(require('./markdown-it-frontmatter'))
|
this.md.use(require('./markdown-it-frontmatter'))
|
||||||
|
|
||||||
@@ -149,9 +165,9 @@ class Markdown {
|
|||||||
const content = token.content.split('\n').slice(0, -1).map(line => {
|
const content = token.content.split('\n').slice(0, -1).map(line => {
|
||||||
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
|
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
|
||||||
if (match) {
|
if (match) {
|
||||||
return match[1]
|
return mdurl.encode(match[1])
|
||||||
} else {
|
} else {
|
||||||
return line
|
return mdurl.encode(line)
|
||||||
}
|
}
|
||||||
}).join('\n')
|
}).join('\n')
|
||||||
|
|
||||||
@@ -181,32 +197,47 @@ class Markdown {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||||
this.md.use(require('markdown-it-plantuml'), '', {
|
const plantuml = require('markdown-it-plantuml')
|
||||||
generateSource: function (umlCode) {
|
const plantUmlStripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
||||||
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
const plantUmlServerAddress = plantUmlStripTrailingSlash(config.preview.plantUMLServerAddress)
|
||||||
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg'
|
const parsePlantUml = function (umlCode, openMarker, closeMarker, type) {
|
||||||
const s = unescape(encodeURIComponent(umlCode))
|
const s = unescape(encodeURIComponent(umlCode))
|
||||||
const zippedCode = deflate.encode64(
|
const zippedCode = deflate.encode64(
|
||||||
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9)
|
||||||
)
|
)
|
||||||
return `${serverAddress}/${zippedCode}`
|
return `${plantUmlServerAddress}/${type}/${zippedCode}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.md.use(plantuml, {
|
||||||
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startuml', '@enduml', 'svg')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ditaa support
|
// Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
||||||
this.md.use(require('markdown-it-plantuml'), {
|
this.md.use(plantuml, {
|
||||||
openMarker: '@startditaa',
|
openMarker: '@startditaa',
|
||||||
closeMarker: '@endditaa',
|
closeMarker: '@endditaa',
|
||||||
generateSource: function (umlCode) {
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
|
||||||
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
})
|
||||||
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
|
||||||
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
|
// Mindmap support
|
||||||
const s = unescape(encodeURIComponent(umlCode))
|
this.md.use(plantuml, {
|
||||||
const zippedCode = deflate.encode64(
|
openMarker: '@startmindmap',
|
||||||
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9)
|
closeMarker: '@endmindmap',
|
||||||
)
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg')
|
||||||
return `${serverAddress}/${zippedCode}`
|
})
|
||||||
}
|
|
||||||
|
// WBS support
|
||||||
|
this.md.use(plantuml, {
|
||||||
|
openMarker: '@startwbs',
|
||||||
|
closeMarker: '@endwbs',
|
||||||
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Gantt support
|
||||||
|
this.md.use(plantuml, {
|
||||||
|
openMarker: '@startgantt',
|
||||||
|
closeMarker: '@endgantt',
|
||||||
|
generateSource: (umlCode) => parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Override task item
|
// Override task item
|
||||||
@@ -287,7 +318,9 @@ class Markdown {
|
|||||||
case 'list_item_open':
|
case 'list_item_open':
|
||||||
case 'paragraph_open':
|
case 'paragraph_open':
|
||||||
case 'table_open':
|
case 'table_open':
|
||||||
token.attrPush(['data-line', token.map[0]])
|
if (token.map) {
|
||||||
|
token.attrPush(['data-line', token.map[0]])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { hashHistory } from 'react-router'
|
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
|
export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||||
@@ -28,10 +29,10 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
|
|||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
|
|
||||||
hashHistory.push({
|
dispatch(push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: { key: noteHash }
|
search: queryString.stringify({ key: noteHash })
|
||||||
})
|
}))
|
||||||
ee.emit('list:jump', noteHash)
|
ee.emit('list:jump', noteHash)
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
})
|
})
|
||||||
@@ -70,10 +71,10 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
|
|||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
hashHistory.push({
|
dispatch(push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: { key: noteHash }
|
search: queryString.stringify({ key: noteHash })
|
||||||
})
|
}))
|
||||||
ee.emit('list:jump', noteHash)
|
ee.emit('list:jump', noteHash)
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import diacritics from 'diacritics-map'
|
|
||||||
|
|
||||||
function replaceDiacritics (str) {
|
|
||||||
return str.replace(/[À-ž]/g, function (ch) {
|
|
||||||
return diacritics[ch] || ch
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function slugify (title) {
|
module.exports = function slugify (title) {
|
||||||
let slug = title.trim()
|
const slug = encodeURI(
|
||||||
|
title.trim()
|
||||||
|
.replace(/^\s+/, '')
|
||||||
|
.replace(/\s+$/, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g, '')
|
||||||
|
)
|
||||||
|
|
||||||
slug = replaceDiacritics(slug)
|
return slug
|
||||||
|
|
||||||
slug = slug.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
|
|
||||||
|
|
||||||
return encodeURI(slug).replace(/\-+$/, '')
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ let self
|
|||||||
|
|
||||||
function getAvailableDictionaries () {
|
function getAvailableDictionaries () {
|
||||||
return [
|
return [
|
||||||
{label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED},
|
{label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED},
|
||||||
{label: i18n.__('English'), value: 'en_GB'},
|
{label: i18n.__('English'), value: 'en_GB'},
|
||||||
{label: i18n.__('German'), value: 'de_DE'},
|
{label: i18n.__('German'), value: 'de_DE'},
|
||||||
{label: i18n.__('French'), value: 'fr_FR'}
|
{label: i18n.__('French'), value: 'fr_FR'}
|
||||||
|
|||||||
9
browser/lib/turndown.js
Normal file
9
browser/lib/turndown.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const TurndownService = require('turndown')
|
||||||
|
const { gfm } = require('turndown-plugin-gfm')
|
||||||
|
|
||||||
|
export const createTurndownService = function () {
|
||||||
|
const turndown = new TurndownService()
|
||||||
|
turndown.use(gfm)
|
||||||
|
turndown.remove('script')
|
||||||
|
return turndown
|
||||||
|
}
|
||||||
@@ -132,8 +132,28 @@ export function isObjectEqual (a, b) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMarkdownTitleURL (str) {
|
||||||
|
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanFileSize (bytes) {
|
||||||
|
const threshold = 1000
|
||||||
|
if (Math.abs(bytes) < threshold) {
|
||||||
|
return bytes + ' B'
|
||||||
|
}
|
||||||
|
var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
var u = -1
|
||||||
|
do {
|
||||||
|
bytes /= threshold
|
||||||
|
++u
|
||||||
|
} while (Math.abs(bytes) >= threshold && u < units.length - 1)
|
||||||
|
return bytes.toFixed(1) + ' ' + units[u]
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
lastFindInArray,
|
lastFindInArray,
|
||||||
escapeHtmlCharacters,
|
escapeHtmlCharacters,
|
||||||
isObjectEqual
|
isObjectEqual,
|
||||||
|
isMarkdownTitleURL,
|
||||||
|
humanFileSize
|
||||||
}
|
}
|
||||||
|
|||||||
69
browser/main/Detail/FromUrlButton.js
Normal file
69
browser/main/Detail/FromUrlButton.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './FromUrlButton.styl'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
class FromUrlButton extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isActive: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown (e) {
|
||||||
|
this.setState({
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp (e) {
|
||||||
|
this.setState({
|
||||||
|
isActive: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave (e) {
|
||||||
|
this.setState({
|
||||||
|
isActive: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { className } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={_.isString(className)
|
||||||
|
? 'FromUrlButton ' + className
|
||||||
|
: 'FromUrlButton'
|
||||||
|
}
|
||||||
|
styleName={this.state.isActive || this.props.isActive
|
||||||
|
? 'root--active'
|
||||||
|
: 'root'
|
||||||
|
}
|
||||||
|
onMouseDown={(e) => this.handleMouseDown(e)}
|
||||||
|
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||||
|
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
||||||
|
onClick={this.props.onClick}>
|
||||||
|
<img styleName='icon'
|
||||||
|
src={this.state.isActive || this.props.isActive
|
||||||
|
? '../resources/icon/icon-external.svg'
|
||||||
|
: '../resources/icon/icon-external.svg'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span styleName='tooltip'>{i18n.__('Convert URL to Markdown')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FromUrlButton.propTypes = {
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
className: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(FromUrlButton, styles)
|
||||||
41
browser/main/Detail/FromUrlButton.styl
Normal file
41
browser/main/Detail/FromUrlButton.styl
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.root
|
||||||
|
top 45px
|
||||||
|
topBarButtonRight()
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 125px
|
||||||
|
width 90px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
@extend .root
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-favorite-star-button-color
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
|
|
||||||
|
.icon
|
||||||
|
transition transform 0.15s
|
||||||
|
height 13px
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
topBarButtonDark()
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
@@ -11,7 +11,7 @@ const FullscreenButton = ({
|
|||||||
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
||||||
return (
|
return (
|
||||||
<button styleName='control-fullScreenButton' title={i18n.__('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 src='../resources/icon/icon-full.svg' />
|
||||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class InfoPanel extends React.Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
|
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf, wordCount, letterCount, type, print
|
||||||
} = this.props
|
} = this.props
|
||||||
return (
|
return (
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||||
@@ -60,7 +60,7 @@ class InfoPanel extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
|
<input styleName='infoPanel-noteLink' ref='noteLink' defaultValue={noteLink} onClick={(e) => { e.target.select() }} />
|
||||||
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
||||||
<i className='fa fa-clipboard' />
|
<i className='fa fa-clipboard' />
|
||||||
</button>
|
</button>
|
||||||
@@ -85,6 +85,11 @@ class InfoPanel extends React.Component {
|
|||||||
<p>{i18n.__('.html')}</p>
|
<p>{i18n.__('.html')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
|
||||||
|
<i className='fa fa-file-pdf-o' />
|
||||||
|
<p>{i18n.__('.pdf')}</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
|
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
|
||||||
<i className='fa fa-print' />
|
<i className='fa fa-print' />
|
||||||
<p>{i18n.__('Print')}</p>
|
<p>{i18n.__('Print')}</p>
|
||||||
@@ -104,6 +109,7 @@ InfoPanel.propTypes = {
|
|||||||
exportAsMd: PropTypes.func.isRequired,
|
exportAsMd: PropTypes.func.isRequired,
|
||||||
exportAsTxt: PropTypes.func.isRequired,
|
exportAsTxt: PropTypes.func.isRequired,
|
||||||
exportAsHtml: PropTypes.func.isRequired,
|
exportAsHtml: PropTypes.func.isRequired,
|
||||||
|
exportAsPdf: PropTypes.func.isRequired,
|
||||||
wordCount: PropTypes.number,
|
wordCount: PropTypes.number,
|
||||||
letterCount: PropTypes.number,
|
letterCount: PropTypes.number,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
right 25px
|
right 25px
|
||||||
position absolute
|
position absolute
|
||||||
padding 20px 25px 0 25px
|
padding 20px 25px 0 25px
|
||||||
width 300px
|
// width 300px
|
||||||
overflow auto
|
overflow auto
|
||||||
background-color $ui-noteList-backgroundColor
|
background-color $ui-noteList-backgroundColor
|
||||||
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import styles from './InfoPanel.styl'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoPanelTrashed = ({
|
const InfoPanelTrashed = ({
|
||||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
|
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf
|
||||||
}) => (
|
}) => (
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
||||||
<div>
|
<div>
|
||||||
@@ -46,7 +46,7 @@ const InfoPanelTrashed = ({
|
|||||||
<p>.html</p>
|
<p>.html</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--unable'>
|
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
|
||||||
<i className='fa fa-file-pdf-o' />
|
<i className='fa fa-file-pdf-o' />
|
||||||
<p>.pdf</p>
|
<p>.pdf</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -61,7 +61,8 @@ InfoPanelTrashed.propTypes = {
|
|||||||
createdAt: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
exportAsMd: PropTypes.func.isRequired,
|
exportAsMd: PropTypes.func.isRequired,
|
||||||
exportAsTxt: PropTypes.func.isRequired,
|
exportAsTxt: PropTypes.func.isRequired,
|
||||||
exportAsHtml: PropTypes.func.isRequired
|
exportAsHtml: PropTypes.func.isRequired,
|
||||||
|
exportAsPdf: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(InfoPanelTrashed, styles)
|
export default CSSModules(InfoPanelTrashed, styles)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import StarButton from './StarButton'
|
|||||||
import TagSelect from './TagSelect'
|
import TagSelect from './TagSelect'
|
||||||
import FolderSelect from './FolderSelect'
|
import FolderSelect from './FolderSelect'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import { hashHistory } from 'react-router'
|
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import markdown from 'browser/lib/markdownTextHelper'
|
import markdown from 'browser/lib/markdownTextHelper'
|
||||||
import StatusBar from '../StatusBar'
|
import StatusBar from '../StatusBar'
|
||||||
@@ -30,6 +29,9 @@ import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
|||||||
import striptags from 'striptags'
|
import striptags from 'striptags'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
import { replace } from 'connected-react-router'
|
||||||
|
import ToggleDirectionButton from 'browser/main/Detail/ToggleDirectionButton'
|
||||||
|
|
||||||
class MarkdownNoteDetail extends React.Component {
|
class MarkdownNoteDetail extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -45,7 +47,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
isLockButtonShown: props.config.editor.type !== 'SPLIT',
|
isLockButtonShown: props.config.editor.type !== 'SPLIT',
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
editorType: props.config.editor.type,
|
editorType: props.config.editor.type,
|
||||||
switchPreview: props.config.editor.switchPreview
|
switchPreview: props.config.editor.switchPreview,
|
||||||
|
RTL: false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchTimer = null
|
this.dispatchTimer = null
|
||||||
@@ -60,15 +63,13 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
|
ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection())
|
||||||
ee.on('topbar:togglemodebutton', () => {
|
ee.on('topbar:togglemodebutton', () => {
|
||||||
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
||||||
this.handleSwitchMode(reversedType)
|
this.handleSwitchMode(reversedType)
|
||||||
})
|
})
|
||||||
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
|
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
|
||||||
ee.on('code:generate-toc', this.generateToc)
|
ee.on('code:generate-toc', this.generateToc)
|
||||||
|
|
||||||
// Focus content if using blur or double click
|
|
||||||
if (this.state.switchPreview === 'BLUR' || this.state.switchPreview === 'DBL_CLICK') this.focus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
@@ -83,10 +84,25 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
if (this.refs.tags) this.refs.tags.reset()
|
if (this.refs.tags) this.refs.tags.reset()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Focus content if using blur or double click
|
||||||
|
// --> Moved here from componentDidMount so a re-render during search won't set focus to the editor
|
||||||
|
const {switchPreview} = nextProps.config.editor
|
||||||
|
|
||||||
|
if (this.state.switchPreview !== switchPreview) {
|
||||||
|
this.setState({
|
||||||
|
switchPreview
|
||||||
|
})
|
||||||
|
if (switchPreview === 'BLUR' || switchPreview === 'DBL_CLICK') {
|
||||||
|
console.log('setting focus', switchPreview)
|
||||||
|
this.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
|
ee.on('topbar:toggledirectionbutton', this.handleSwitchDirection)
|
||||||
ee.off('code:generate-toc', this.generateToc)
|
ee.off('code:generate-toc', this.generateToc)
|
||||||
if (this.saveQueue != null) this.saveNow()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
}
|
}
|
||||||
@@ -159,12 +175,12 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
originNote: note,
|
originNote: note,
|
||||||
note: newNote
|
note: newNote
|
||||||
})
|
})
|
||||||
hashHistory.replace({
|
dispatch(replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: newNote.key
|
key: newNote.key
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
this.setState({
|
this.setState({
|
||||||
isMovingNote: false
|
isMovingNote: false
|
||||||
})
|
})
|
||||||
@@ -201,6 +217,10 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.emit('export:save-html')
|
ee.emit('export:save-html')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportAsPdf () {
|
||||||
|
ee.emit('export:save-pdf')
|
||||||
|
}
|
||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
// tab key
|
// tab key
|
||||||
@@ -294,7 +314,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToggleLockButton () {
|
getToggleLockButton () {
|
||||||
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
|
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteKeyDown (e) {
|
handleDeleteKeyDown (e) {
|
||||||
@@ -338,6 +358,12 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSwitchDirection () {
|
||||||
|
// If in split mode, hide the lock button
|
||||||
|
const direction = this.state.RTL
|
||||||
|
this.setState({ RTL: !direction })
|
||||||
|
}
|
||||||
|
|
||||||
handleDeleteNote () {
|
handleDeleteNote () {
|
||||||
this.handleTrashButtonClick()
|
this.handleTrashButtonClick()
|
||||||
}
|
}
|
||||||
@@ -377,6 +403,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
isLocked={this.state.isLocked}
|
isLocked={this.state.isLocked}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
RTL={this.state.RTL}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <MarkdownSplitEditor
|
return <MarkdownSplitEditor
|
||||||
@@ -388,12 +415,13 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
linesHighlighted={note.linesHighlighted}
|
linesHighlighted={note.linesHighlighted}
|
||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
RTL={this.state.RTL}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { data, location, config } = this.props
|
const { data, dispatch, location, config } = this.props
|
||||||
const { note, editorType } = this.state
|
const { note, editorType } = this.state
|
||||||
const storageKey = note.storage
|
const storageKey = note.storage
|
||||||
const folderKey = note.folder
|
const folderKey = note.folder
|
||||||
@@ -426,13 +454,14 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
exportAsHtml={this.exportAsHtml}
|
exportAsHtml={this.exportAsHtml}
|
||||||
exportAsMd={this.exportAsMd}
|
exportAsMd={this.exportAsMd}
|
||||||
exportAsTxt={this.exportAsTxt}
|
exportAsTxt={this.exportAsTxt}
|
||||||
|
exportAsPdf={this.exportAsPdf}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
const detailTopBar = <div styleName='info'>
|
const detailTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<div styleName='info-left-top'>
|
<div>
|
||||||
<FolderSelect styleName='info-left-top-folderSelect'
|
<FolderSelect styleName='info-left-top-folderSelect'
|
||||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||||
ref='folder'
|
ref='folder'
|
||||||
@@ -447,6 +476,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||||
data={data}
|
data={data}
|
||||||
|
dispatch={dispatch}
|
||||||
onChange={this.handleUpdateTag.bind(this)}
|
onChange={this.handleUpdateTag.bind(this)}
|
||||||
coloredTags={config.coloredTags}
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
@@ -454,6 +484,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||||
|
<ToggleDirectionButton onClick={(e) => this.handleSwitchDirection(e)} isRTL={this.state.RTL} />
|
||||||
<StarButton
|
<StarButton
|
||||||
onClick={(e) => this.handleStarButtonClick(e)}
|
onClick={(e) => this.handleStarButtonClick(e)}
|
||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
@@ -466,7 +497,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
onFocus={(e) => this.handleFocus(e)}
|
onFocus={(e) => this.handleFocus(e)}
|
||||||
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src={imgSrc} />
|
<img src={imgSrc} />
|
||||||
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
|
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -486,18 +517,20 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.exportAsMd}
|
exportAsMd={this.exportAsMd}
|
||||||
exportAsTxt={this.exportAsTxt}
|
exportAsTxt={this.exportAsTxt}
|
||||||
exportAsHtml={this.exportAsHtml}
|
exportAsHtml={this.exportAsHtml}
|
||||||
wordCount={note.content.split(' ').length}
|
exportAsPdf={this.exportAsPdf}
|
||||||
|
wordCount={note.content.trim().split(/\s+/g).length}
|
||||||
letterCount={note.content.replace(/\r?\n/g, '').length}
|
letterCount={note.content.replace(/\r?\n/g, '').length}
|
||||||
type={note.type}
|
type={note.type}
|
||||||
print={this.print}
|
print={this.print}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
.control-lockButton
|
.control-lockButton
|
||||||
topBarButtonRight()
|
topBarButtonRight()
|
||||||
position absolute
|
position absolute
|
||||||
right 225px
|
right 265px
|
||||||
&:hover .tooltip
|
&:hover .tooltip
|
||||||
opacity 1
|
opacity 1
|
||||||
|
|
||||||
@@ -76,4 +76,4 @@ for theme in 'solarized-dark' 'dracula'
|
|||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|
||||||
for theme in $themes
|
for theme in $themes
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ $info-margin-under-border = 30px
|
|||||||
padding 0 20px
|
padding 0 20px
|
||||||
z-index 99
|
z-index 99
|
||||||
|
|
||||||
|
.info > div
|
||||||
|
> button
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
|
> img, span
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
|
|
||||||
.info-left
|
.info-left
|
||||||
padding 0 10px
|
padding 0 10px
|
||||||
width 100%
|
width 100%
|
||||||
@@ -104,4 +112,4 @@ for theme in 'solarized-dark' 'dracula'
|
|||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|
||||||
for theme in $themes
|
for theme in $themes
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const PermanentDeleteButton = ({
|
|||||||
<button styleName='control-trashButton--in-trash'
|
<button styleName='control-trashButton--in-trash'
|
||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img src='../resources/icon/icon-trash.svg' />
|
||||||
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import StarButton from './StarButton'
|
|||||||
import TagSelect from './TagSelect'
|
import TagSelect from './TagSelect'
|
||||||
import FolderSelect from './FolderSelect'
|
import FolderSelect from './FolderSelect'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import {hashHistory} from 'react-router'
|
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
import 'codemirror-mode-elixir'
|
import 'codemirror-mode-elixir'
|
||||||
@@ -18,7 +17,6 @@ import context from 'browser/lib/context'
|
|||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
||||||
import convertModeName from 'browser/lib/convertModeName'
|
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import FullscreenButton from './FullscreenButton'
|
import FullscreenButton from './FullscreenButton'
|
||||||
import TrashButton from './TrashButton'
|
import TrashButton from './TrashButton'
|
||||||
@@ -31,6 +29,8 @@ import { formatDate } from 'browser/lib/date-formatter'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
import { replace } from 'connected-react-router'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
@@ -166,12 +166,12 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
originNote: note,
|
originNote: note,
|
||||||
note: newNote
|
note: newNote
|
||||||
})
|
})
|
||||||
hashHistory.replace({
|
dispatch(replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: newNote.key
|
key: newNote.key
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
this.setState({
|
this.setState({
|
||||||
isMovingNote: false
|
isMovingNote: false
|
||||||
})
|
})
|
||||||
@@ -518,6 +518,19 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleWrapLineButtonClick (e) {
|
||||||
|
context.popup([
|
||||||
|
{
|
||||||
|
label: 'on',
|
||||||
|
click: (e) => this.handleWrapLineItemClick(e, true)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'off',
|
||||||
|
click: (e) => this.handleWrapLineItemClick(e, false)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
handleIndentSizeItemClick (e, indentSize) {
|
handleIndentSizeItemClick (e, indentSize) {
|
||||||
const { config, dispatch } = this.props
|
const { config, dispatch } = this.props
|
||||||
const editor = Object.assign({}, config.editor, {
|
const editor = Object.assign({}, config.editor, {
|
||||||
@@ -550,6 +563,22 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleWrapLineItemClick (e, lineWrapping) {
|
||||||
|
const { config, dispatch } = this.props
|
||||||
|
const editor = Object.assign({}, config.editor, {
|
||||||
|
lineWrapping
|
||||||
|
})
|
||||||
|
ConfigManager.set({
|
||||||
|
editor
|
||||||
|
})
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config: {
|
||||||
|
editor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
this.refs.description.focus()
|
this.refs.description.focus()
|
||||||
}
|
}
|
||||||
@@ -657,6 +686,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
'export-txt': 'Text export',
|
'export-txt': 'Text export',
|
||||||
'export-md': 'Markdown export',
|
'export-md': 'Markdown export',
|
||||||
'export-html': 'HTML export',
|
'export-html': 'HTML export',
|
||||||
|
'export-pdf': 'PDF export',
|
||||||
'print': 'Print'
|
'print': 'Print'
|
||||||
})[msg]
|
})[msg]
|
||||||
|
|
||||||
@@ -669,7 +699,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { data, config, location } = this.props
|
const { data, dispatch, config, location } = this.props
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
const storageKey = note.storage
|
const storageKey = note.storage
|
||||||
@@ -719,6 +749,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
|
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
|
||||||
value={snippet.content}
|
value={snippet.content}
|
||||||
linesHighlighted={snippet.linesHighlighted}
|
linesHighlighted={snippet.linesHighlighted}
|
||||||
|
lineWrapping={config.editor.lineWrapping}
|
||||||
theme={config.editor.theme}
|
theme={config.editor.theme}
|
||||||
fontFamily={config.editor.fontFamily}
|
fontFamily={config.editor.fontFamily}
|
||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
@@ -770,13 +801,14 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
exportAsTxt={this.showWarning}
|
exportAsTxt={this.showWarning}
|
||||||
exportAsHtml={this.showWarning}
|
exportAsHtml={this.showWarning}
|
||||||
|
exportAsPdf={this.showWarning}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
const detailTopBar = <div styleName='info'>
|
const detailTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<div styleName='info-left-top'>
|
<div>
|
||||||
<FolderSelect styleName='info-left-top-folderSelect'
|
<FolderSelect styleName='info-left-top-folderSelect'
|
||||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||||
ref='folder'
|
ref='folder'
|
||||||
@@ -791,6 +823,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||||
data={data}
|
data={data}
|
||||||
|
dispatch={dispatch}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
coloredTags={config.coloredTags}
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
@@ -812,12 +845,13 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
exportAsTxt={this.showWarning}
|
exportAsTxt={this.showWarning}
|
||||||
exportAsHtml={this.showWarning}
|
exportAsHtml={this.showWarning}
|
||||||
|
exportAsPdf={this.showWarning}
|
||||||
type={note.type}
|
type={note.type}
|
||||||
print={this.showWarning}
|
print={this.showWarning}
|
||||||
/>
|
/>
|
||||||
@@ -896,6 +930,12 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
size: {config.editor.indentSize}
|
size: {config.editor.indentSize}
|
||||||
<i className='fa fa-caret-down' />
|
<i className='fa fa-caret-down' />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => this.handleWrapLineButtonClick(e)}
|
||||||
|
>
|
||||||
|
Wrap Line: {config.editor.lineWrapping ? 'on' : 'off'}
|
||||||
|
<i className='fa fa-caret-down' />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusBar
|
<StatusBar
|
||||||
|
|||||||
@@ -42,4 +42,4 @@ body[data-theme="dark"]
|
|||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
&:hover
|
&:hover
|
||||||
transition 0.2s
|
transition 0.2s
|
||||||
color alpha($ui-favorite-star-button-color, 0.6)
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import Autosuggest from 'react-autosuggest'
|
import Autosuggest from 'react-autosuggest'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
class TagSelect extends React.Component {
|
class TagSelect extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -96,8 +97,11 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTagLabelClick (tag) {
|
handleTagLabelClick (tag) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push(`/tags/${tag}`)
|
|
||||||
|
// Note: `tag` requires encoding later.
|
||||||
|
// E.g. % in tag is a problem (see issue #3170) - encodeURIComponent(tag) is not working.
|
||||||
|
dispatch(push(`/tags/${tag}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagRemoveButtonClick (tag) {
|
handleTagRemoveButtonClick (tag) {
|
||||||
@@ -255,11 +259,8 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TagSelect.contextTypes = {
|
|
||||||
router: PropTypes.shape({})
|
|
||||||
}
|
|
||||||
|
|
||||||
TagSelect.propTypes = {
|
TagSelect.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
value: PropTypes.arrayOf(PropTypes.string),
|
value: PropTypes.arrayOf(PropTypes.string),
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
|||||||
26
browser/main/Detail/ToggleDirectionButton.js
Normal file
26
browser/main/Detail/ToggleDirectionButton.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './ToggleDirectionButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const ToggleDirectionButton = ({
|
||||||
|
onClick, isRTL
|
||||||
|
}) => (
|
||||||
|
<div styleName='control-toggleModeButton'>
|
||||||
|
<div styleName={isRTL ? 'active' : undefined} onClick={() => onClick()}>
|
||||||
|
<img src={!isRTL ? '../resources/icon/icon-left-to-right.svg' : ''} />
|
||||||
|
</div>
|
||||||
|
<div styleName={!isRTL ? 'active' : undefined} onClick={() => onClick()}>
|
||||||
|
<img src={!isRTL ? '' : '../resources/icon/icon-right-to-left.svg'} />
|
||||||
|
</div>
|
||||||
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Direction')}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
ToggleDirectionButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
isRTL: PropTypes.string.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(ToggleDirectionButton, styles)
|
||||||
85
browser/main/Detail/ToggleDirectionButton.styl
Normal file
85
browser/main/Detail/ToggleDirectionButton.styl
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
.control-toggleModeButton
|
||||||
|
height 25px
|
||||||
|
border-radius 50px
|
||||||
|
background-color #F4F4F4
|
||||||
|
width 52px
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
position: relative
|
||||||
|
top 2px
|
||||||
|
margin-left 5px
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
width 33px
|
||||||
|
height 24px
|
||||||
|
box-shadow 2px 0px 7px #eee
|
||||||
|
z-index 1
|
||||||
|
|
||||||
|
div
|
||||||
|
width 40px
|
||||||
|
height 100%
|
||||||
|
border-radius 50%
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 33px
|
||||||
|
left -10px
|
||||||
|
z-index 200
|
||||||
|
width 80px
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
.tooltip:lang(ja)
|
||||||
|
@extend .tooltip
|
||||||
|
left -8px
|
||||||
|
width 70px
|
||||||
|
|
||||||
|
.control-toggleModeButton
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
|
> div img
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.control-fullScreenButton
|
||||||
|
topBarButtonDark()
|
||||||
|
|
||||||
|
.control-toggleModeButton
|
||||||
|
background-color #3A404C
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
box-shadow 2px 0px 7px #444444
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.control-toggleModeButton
|
||||||
|
background-color #002B36
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
box-shadow 2px 0px 7px #222222
|
||||||
|
|
||||||
|
apply-theme(theme)
|
||||||
|
body[data-theme={theme}]
|
||||||
|
.control-toggleModeButton
|
||||||
|
background-color get-theme-var(theme, 'borderColor')
|
||||||
|
.active
|
||||||
|
background-color get-theme-var(theme, 'active-color')
|
||||||
|
box-shadow 2px 0px 7px #222222
|
||||||
|
|
||||||
|
for theme in 'dracula'
|
||||||
|
apply-theme(theme)
|
||||||
|
|
||||||
|
for theme in $themes
|
||||||
|
apply-theme(theme)
|
||||||
@@ -8,11 +8,11 @@ const ToggleModeButton = ({
|
|||||||
onClick, editorType
|
onClick, editorType
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName='control-toggleModeButton'>
|
<div styleName='control-toggleModeButton'>
|
||||||
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
<div styleName={editorType === 'SPLIT' ? 'active' : undefined} onClick={() => onClick('SPLIT')}>
|
||||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
<img src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
<img src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||||
</div>
|
</div>
|
||||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +20,7 @@ const ToggleModeButton = ({
|
|||||||
|
|
||||||
ToggleModeButton.propTypes = {
|
ToggleModeButton.propTypes = {
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
editorType: PropTypes.string.Required
|
editorType: PropTypes.string.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(ToggleModeButton, styles)
|
export default CSSModules(ToggleModeButton, styles)
|
||||||
|
|||||||
@@ -26,6 +26,13 @@
|
|||||||
&:hover .tooltip
|
&:hover .tooltip
|
||||||
opacity 1
|
opacity 1
|
||||||
|
|
||||||
|
.control-toggleModeButton
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
|
> div img
|
||||||
|
-webkit-user-drag none
|
||||||
|
user-select none
|
||||||
|
|
||||||
.tooltip
|
.tooltip
|
||||||
tooltip()
|
tooltip()
|
||||||
position absolute
|
position absolute
|
||||||
@@ -74,4 +81,4 @@ for theme in 'dracula'
|
|||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|
||||||
for theme in $themes
|
for theme in $themes
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const TrashButton = ({
|
|||||||
<button styleName='control-trashButton'
|
<button styleName='control-trashButton'
|
||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img src='../resources/icon/icon-trash.svg' />
|
||||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import StatusBar from '../StatusBar'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import debounceRender from 'react-debounce-render'
|
import debounceRender from 'react-debounce-render'
|
||||||
import searchFromNotes from 'browser/lib/search'
|
import searchFromNotes from 'browser/lib/search'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
@@ -36,11 +37,11 @@ class Detail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { location, data, params, config } = this.props
|
const { location, data, match: { params }, config } = this.props
|
||||||
|
const noteKey = location.search !== '' && queryString.parse(location.search).key
|
||||||
let note = null
|
let note = null
|
||||||
|
|
||||||
if (location.query.key != null) {
|
if (location.search !== '') {
|
||||||
const noteKey = location.query.key
|
|
||||||
const allNotes = data.noteMap.map(note => note)
|
const allNotes = data.noteMap.map(note => note)
|
||||||
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
|
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||||
let displayedNotes = allNotes
|
let displayedNotes = allNotes
|
||||||
@@ -49,16 +50,14 @@ class Detail extends React.Component {
|
|||||||
const searchStr = params.searchword
|
const searchStr = params.searchword
|
||||||
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
|
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
|
||||||
: searchFromNotes(allNotes, searchStr)
|
: searchFromNotes(allNotes, searchStr)
|
||||||
}
|
} else if (location.pathname.match(/^\/tags/)) {
|
||||||
|
|
||||||
if (location.pathname.match(/\/tags/)) {
|
|
||||||
const listOfTags = params.tagname.split(' ')
|
const listOfTags = params.tagname.split(' ')
|
||||||
displayedNotes = data.noteMap.map(note => note).filter(note =>
|
displayedNotes = data.noteMap.map(note => note).filter(note =>
|
||||||
listOfTags.every(tag => note.tags.includes(tag))
|
listOfTags.every(tag => note.tags.includes(tag))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname.match(/\/trashed/)) {
|
if (location.pathname.match(/^\/trashed/)) {
|
||||||
displayedNotes = trashedNotes
|
displayedNotes = trashedNotes
|
||||||
} else {
|
} else {
|
||||||
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
|
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
|
||||||
|
|||||||
16
browser/main/DevTools/index.dev.js
Normal file
16
browser/main/DevTools/index.dev.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { createDevTools } from 'redux-devtools'
|
||||||
|
import LogMonitor from 'redux-devtools-log-monitor'
|
||||||
|
import DockMonitor from 'redux-devtools-dock-monitor'
|
||||||
|
|
||||||
|
const DevTools = createDevTools(
|
||||||
|
<DockMonitor
|
||||||
|
toggleVisibilityKey='ctrl-h'
|
||||||
|
changePositionKey='ctrl-q'
|
||||||
|
defaultIsVisible={false}
|
||||||
|
>
|
||||||
|
<LogMonitor theme='tomorrow' />
|
||||||
|
</DockMonitor>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default DevTools
|
||||||
8
browser/main/DevTools/index.js
Normal file
8
browser/main/DevTools/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
module.exports = require('./index.dev').default
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
module.exports = require('./index.prod').default
|
||||||
|
}
|
||||||
6
browser/main/DevTools/index.prod.js
Normal file
6
browser/main/DevTools/index.prod.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const DevTools = () => <div />
|
||||||
|
DevTools.instrument = () => {}
|
||||||
|
|
||||||
|
export default DevTools
|
||||||
@@ -12,12 +12,13 @@ import _ from 'lodash'
|
|||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import { hashHistory } from 'react-router'
|
import { store } from 'browser/main/store'
|
||||||
import store from 'browser/main/store'
|
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import { getLocales } from 'browser/lib/Languages'
|
import { getLocales } from 'browser/lib/Languages'
|
||||||
import applyShortcuts from 'browser/main/lib/shortcutManager'
|
import applyShortcuts from 'browser/main/lib/shortcutManager'
|
||||||
import uiThemes from 'browser/lib/ui-themes'
|
import uiThemes from 'browser/lib/ui-themes'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
@@ -103,7 +104,7 @@ class Main extends React.Component {
|
|||||||
{
|
{
|
||||||
name: 'example.js',
|
name: 'example.js',
|
||||||
mode: 'javascript',
|
mode: 'javascript',
|
||||||
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)",
|
content: "var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)",
|
||||||
linesHighlighted: []
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -133,7 +134,7 @@ class Main extends React.Component {
|
|||||||
.then(() => data.storage)
|
.then(() => data.storage)
|
||||||
})
|
})
|
||||||
.then(storage => {
|
.then(storage => {
|
||||||
hashHistory.push('/storages/' + storage.key)
|
store.dispatch(push('/storages/' + storage.key))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
throw err
|
throw err
|
||||||
@@ -168,6 +169,7 @@ class Main extends React.Component {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
||||||
|
|
||||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||||
@@ -310,7 +312,7 @@ class Main extends React.Component {
|
|||||||
onMouseUp={e => this.handleMouseUp(e)}
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
>
|
>
|
||||||
<SideNav
|
<SideNav
|
||||||
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
|
{..._.pick(this.props, ['dispatch', 'data', 'config', 'match', 'location'])}
|
||||||
width={this.state.navWidth}
|
width={this.state.navWidth}
|
||||||
/>
|
/>
|
||||||
{!config.isSideNavFolded &&
|
{!config.isSideNavFolded &&
|
||||||
@@ -340,7 +342,7 @@ class Main extends React.Component {
|
|||||||
'dispatch',
|
'dispatch',
|
||||||
'config',
|
'config',
|
||||||
'data',
|
'data',
|
||||||
'params',
|
'match',
|
||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
@@ -350,7 +352,7 @@ class Main extends React.Component {
|
|||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
'config',
|
'config',
|
||||||
'params',
|
'match',
|
||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
@@ -372,7 +374,7 @@ class Main extends React.Component {
|
|||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
'config',
|
'config',
|
||||||
'params',
|
'match',
|
||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
||||||
|
|||||||
@@ -21,23 +21,20 @@ class NewNoteButton extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.newNoteHandler = () => {
|
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
|
||||||
this.handleNewNoteButtonClick()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
eventEmitter.on('top:new-note', this.newNoteHandler)
|
eventEmitter.on('top:new-note', this.handleNewNoteButtonClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
eventEmitter.off('top:new-note', this.newNoteHandler)
|
eventEmitter.off('top:new-note', this.handleNewNoteButtonClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewNoteButtonClick (e) {
|
handleNewNoteButtonClick (e) {
|
||||||
const { location, params, dispatch, config } = this.props
|
const { location, dispatch, match: { params }, config } = this.props
|
||||||
const { storage, folder } = this.resolveTargetFolder()
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
|
|
||||||
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||||
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
||||||
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||||
@@ -55,9 +52,8 @@ class NewNoteButton extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolveTargetFolder () {
|
resolveTargetFolder () {
|
||||||
const { data, params } = this.props
|
const { data, match: { params } } = this.props
|
||||||
let storage = data.storageMap.get(params.storageKey)
|
let storage = data.storageMap.get(params.storageKey)
|
||||||
|
|
||||||
// Find first storage
|
// Find first storage
|
||||||
if (storage == null) {
|
if (storage == null) {
|
||||||
for (const kv of data.storageMap) {
|
for (const kv of data.storageMap) {
|
||||||
@@ -93,8 +89,8 @@ class NewNoteButton extends React.Component {
|
|||||||
>
|
>
|
||||||
<div styleName='control'>
|
<div styleName='control'>
|
||||||
<button styleName='control-newNoteButton'
|
<button styleName='control-newNoteButton'
|
||||||
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
onClick={this.handleNewNoteButtonClick}>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
|
<img src='../resources/icon/icon-newnote.svg' />
|
||||||
<span styleName='control-newNoteButton-tooltip'>
|
<span styleName='control-newNoteButton-tooltip'>
|
||||||
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -14,23 +14,42 @@ import NoteItemSimple from 'browser/components/NoteItemSimple'
|
|||||||
import searchFromNotes from 'browser/lib/search'
|
import searchFromNotes from 'browser/lib/search'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { hashHistory } from 'react-router'
|
import { push, replace } from 'connected-react-router'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import Markdown from '../../lib/markdown'
|
import Markdown from '../../lib/markdown'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
const WP_POST_PATH = '/wp/v2/posts'
|
const WP_POST_PATH = '/wp/v2/posts'
|
||||||
|
|
||||||
|
const regexMatchStartingTitleNumber = new RegExp('^([0-9]*\.?[0-9]+).*$')
|
||||||
|
|
||||||
function sortByCreatedAt (a, b) {
|
function sortByCreatedAt (a, b) {
|
||||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByAlphabetical (a, b) {
|
function sortByAlphabetical (a, b) {
|
||||||
|
const matchA = regexMatchStartingTitleNumber.exec(a.title)
|
||||||
|
const matchB = regexMatchStartingTitleNumber.exec(b.title)
|
||||||
|
|
||||||
|
if (matchA && matchA.length === 2 && matchB && matchB.length === 2) {
|
||||||
|
// Both note titles are starting with a float. We will compare it now.
|
||||||
|
const floatA = parseFloat(matchA[1])
|
||||||
|
const floatB = parseFloat(matchB[1])
|
||||||
|
|
||||||
|
const diff = floatA - floatB
|
||||||
|
if (diff !== 0) {
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
// The float values are equal. We will compare the full title.
|
||||||
|
}
|
||||||
|
|
||||||
return a.title.localeCompare(b.title)
|
return a.title.localeCompare(b.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +88,7 @@ class NoteList extends React.Component {
|
|||||||
this.importFromFileHandler = this.importFromFile.bind(this)
|
this.importFromFileHandler = this.importFromFile.bind(this)
|
||||||
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
||||||
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
||||||
|
this.handleNoteListBlur = this.handleNoteListBlur.bind(this)
|
||||||
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
||||||
this.cloneNote = this.cloneNote.bind(this)
|
this.cloneNote = this.cloneNote.bind(this)
|
||||||
this.deleteNote = this.deleteNote.bind(this)
|
this.deleteNote = this.deleteNote.bind(this)
|
||||||
@@ -127,15 +147,15 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props
|
const { dispatch, location } = this.props
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const visibleNoteKeys = this.notes.map(note => note.key)
|
const visibleNoteKeys = this.notes && this.notes.map(note => note.key)
|
||||||
const note = this.notes[0]
|
const note = this.notes && this.notes[0]
|
||||||
const prevKey = prevProps.location.query.key
|
const key = location.search && queryString.parse(location.search).key
|
||||||
|
const prevKey = prevProps.location.search && queryString.parse(prevProps.location.search).key
|
||||||
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
||||||
|
|
||||||
if (note && location.query.key == null) {
|
if (note && location.search === '') {
|
||||||
const { router } = this.context
|
|
||||||
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
||||||
|
|
||||||
// A visible note is an active note
|
// A visible note is an active note
|
||||||
@@ -145,17 +165,17 @@ class NoteList extends React.Component {
|
|||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
|
|
||||||
router.replace({
|
dispatch(replace({ // was passed with context - we can use connected router here
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: noteKey
|
key: noteKey
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto scroll
|
// Auto scroll
|
||||||
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
|
if (_.isString(key) && prevKey === key) {
|
||||||
const targetIndex = this.getTargetIndex()
|
const targetIndex = this.getTargetIndex()
|
||||||
if (targetIndex > -1) {
|
if (targetIndex > -1) {
|
||||||
const list = this.refs.list
|
const list = this.refs.list
|
||||||
@@ -176,18 +196,18 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusNote (selectedNoteKeys, noteKey, pathname) {
|
focusNote (selectedNoteKeys, noteKey, pathname) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedNoteKeys
|
selectedNoteKeys
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push({
|
dispatch(push({
|
||||||
pathname,
|
pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: noteKey
|
key: noteKey
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteKeyFromTargetIndex (targetIndex) {
|
getNoteKeyFromTargetIndex (targetIndex) {
|
||||||
@@ -259,13 +279,22 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jumpNoteByHashHandler (event, noteHash) {
|
jumpNoteByHashHandler (event, noteHash) {
|
||||||
|
const { data } = this.props
|
||||||
|
|
||||||
// first argument event isn't used.
|
// first argument event isn't used.
|
||||||
if (this.notes === null || this.notes.length === 0) {
|
if (this.notes === null || this.notes.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedNoteKeys = [noteHash]
|
const selectedNoteKeys = [noteHash]
|
||||||
this.focusNote(selectedNoteKeys, noteHash, '/home')
|
|
||||||
|
let locationToSelect = '/home'
|
||||||
|
const noteByHash = data.noteMap.map((note) => note).find(note => note.key === noteHash)
|
||||||
|
if (noteByHash !== undefined) {
|
||||||
|
locationToSelect = '/storages/' + noteByHash.storage + '/folders/' + noteByHash.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
this.focusNote(selectedNoteKeys, noteHash, locationToSelect)
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
@@ -320,9 +349,15 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotes () {
|
handleNoteListBlur () {
|
||||||
const { data, params, location } = this.props
|
this.setState({
|
||||||
|
shiftKeyDown: false,
|
||||||
|
ctrlKeyDown: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotes () {
|
||||||
|
const { data, match: { params }, location } = this.props
|
||||||
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
|
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
|
||||||
const allNotes = data.noteMap.map((note) => note)
|
const allNotes = data.noteMap.map((note) => note)
|
||||||
this.contextNotes = allNotes
|
this.contextNotes = allNotes
|
||||||
@@ -363,7 +398,7 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
// get notes in the current folder
|
// get notes in the current folder
|
||||||
getContextNotes () {
|
getContextNotes () {
|
||||||
const { data, params } = this.props
|
const { data, match: { params } } = this.props
|
||||||
const storageKey = params.storageKey
|
const storageKey = params.storageKey
|
||||||
const folderKey = params.folderKey
|
const folderKey = params.folderKey
|
||||||
const storage = data.storageMap.get(storageKey)
|
const storage = data.storageMap.get(storageKey)
|
||||||
@@ -403,8 +438,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNoteClick (e, uniqueKey) {
|
handleNoteClick (e, uniqueKey) {
|
||||||
const { router } = this.context
|
const { dispatch, location } = this.props
|
||||||
const { location } = this.props
|
|
||||||
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
||||||
const { ctrlKeyDown, shiftKeyDown } = this.state
|
const { ctrlKeyDown, shiftKeyDown } = this.state
|
||||||
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||||
@@ -455,16 +489,16 @@ class NoteList extends React.Component {
|
|||||||
prevShiftNoteIndex
|
prevShiftNoteIndex
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push({
|
dispatch(push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
key: uniqueKey
|
key: uniqueKey
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortByChange (e) {
|
handleSortByChange (e) {
|
||||||
const { dispatch, params: { folderKey } } = this.props
|
const { dispatch, match: { params: { folderKey } } } = this.props
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
[folderKey]: { sortBy: e.target.value }
|
[folderKey]: { sortBy: e.target.value }
|
||||||
@@ -496,6 +530,7 @@ class NoteList extends React.Component {
|
|||||||
'export-txt': 'Text export',
|
'export-txt': 'Text export',
|
||||||
'export-md': 'Markdown export',
|
'export-md': 'Markdown export',
|
||||||
'export-html': 'HTML export',
|
'export-html': 'HTML export',
|
||||||
|
'export-pdf': 'PDF export',
|
||||||
'print': 'Print'
|
'print': 'Print'
|
||||||
})[msg]
|
})[msg]
|
||||||
|
|
||||||
@@ -716,7 +751,11 @@ class NoteList extends React.Component {
|
|||||||
folder: folder.key,
|
folder: folder.key,
|
||||||
title: firstNote.title + ' ' + i18n.__('copy'),
|
title: firstNote.title + ' ' + i18n.__('copy'),
|
||||||
content: firstNote.content,
|
content: firstNote.content,
|
||||||
linesHighlighted: firstNote.linesHighlighted
|
linesHighlighted: firstNote.linesHighlighted,
|
||||||
|
description: firstNote.description,
|
||||||
|
snippets: firstNote.snippets,
|
||||||
|
tags: firstNote.tags,
|
||||||
|
isStarred: firstNote.isStarred
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
attachmentManagement.cloneAttachments(firstNote, note)
|
attachmentManagement.cloneAttachments(firstNote, note)
|
||||||
@@ -732,10 +771,10 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys: [note.key]
|
selectedNoteKeys: [note.key]
|
||||||
})
|
})
|
||||||
|
|
||||||
hashHistory.push({
|
dispatch(push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {key: note.key}
|
search: queryString.stringify({key: note.key})
|
||||||
})
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,13 +784,13 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
navigate (sender, pathname) {
|
navigate (sender, pathname) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push({
|
dispatch(push({
|
||||||
pathname,
|
pathname,
|
||||||
query: {
|
search: queryString.stringify({
|
||||||
// key: noteKey
|
// key: noteKey
|
||||||
}
|
})
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
save (note) {
|
save (note) {
|
||||||
@@ -881,7 +920,7 @@ class NoteList extends React.Component {
|
|||||||
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
|
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add notes to the current folder
|
// Add notes to the current folder
|
||||||
addNotesFromFiles (filepaths) {
|
addNotesFromFiles (filepaths) {
|
||||||
const { dispatch, location } = this.props
|
const { dispatch, location } = this.props
|
||||||
const { storage, folder } = this.resolveTargetFolder()
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
@@ -905,13 +944,20 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
dataApi.createNote(storage.key, newNote)
|
dataApi.createNote(storage.key, newNote)
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
dispatch({
|
attachmentManagement.importAttachments(note.content, filepath, storage.key, note.key)
|
||||||
type: 'UPDATE_NOTE',
|
.then((newcontent) => {
|
||||||
note: note
|
note.content = newcontent
|
||||||
})
|
|
||||||
hashHistory.push({
|
dataApi.updateNote(storage.key, note.key, note)
|
||||||
pathname: location.pathname,
|
|
||||||
query: {key: getNoteKey(note)}
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
dispatch(push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
search: queryString.stringify({key: getNoteKey(note)})
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -921,14 +967,15 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
getTargetIndex () {
|
getTargetIndex () {
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
|
const key = queryString.parse(location.search).key
|
||||||
const targetIndex = _.findIndex(this.notes, (note) => {
|
const targetIndex = _.findIndex(this.notes, (note) => {
|
||||||
return getNoteKey(note) === location.query.key
|
return getNoteKey(note) === key
|
||||||
})
|
})
|
||||||
return targetIndex
|
return targetIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveTargetFolder () {
|
resolveTargetFolder () {
|
||||||
const { data, params } = this.props
|
const { data, match: { params } } = this.props
|
||||||
let storage = data.storageMap.get(params.storageKey)
|
let storage = data.storageMap.get(params.storageKey)
|
||||||
|
|
||||||
// Find first storage
|
// Find first storage
|
||||||
@@ -976,7 +1023,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { location, config, params: { folderKey } } = this.props
|
const { location, config, match: { params: { folderKey } } } = this.props
|
||||||
let { notes } = this.props
|
let { notes } = this.props
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
||||||
@@ -1099,7 +1146,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
||||||
>
|
>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
<img src='../resources/icon/icon-column.svg' />
|
||||||
</button>
|
</button>
|
||||||
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
|
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
@@ -1107,7 +1154,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
||||||
>
|
>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-column-list.svg' />
|
<img src='../resources/icon/icon-column-list.svg' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1116,6 +1163,7 @@ class NoteList extends React.Component {
|
|||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
||||||
onKeyUp={this.handleNoteListKeyUp}
|
onKeyUp={this.handleNoteListKeyUp}
|
||||||
|
onBlur={this.handleNoteListBlur}
|
||||||
>
|
>
|
||||||
{noteList}
|
{noteList}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const PreferenceButton = ({
|
|||||||
onClick
|
onClick
|
||||||
}) => (
|
}) => (
|
||||||
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
|
<img src='../resources/icon/icon-setting.svg' />
|
||||||
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './StorageItem.styl'
|
import styles from './StorageItem.styl'
|
||||||
import { hashHistory } from 'react-router'
|
|
||||||
import modal from 'browser/main/lib/modal'
|
import modal from 'browser/main/lib/modal'
|
||||||
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
|
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
|
||||||
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
||||||
@@ -12,6 +11,7 @@ import _ from 'lodash'
|
|||||||
import { SortableElement } from 'react-sortable-hoc'
|
import { SortableElement } from 'react-sortable-hoc'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
@@ -134,14 +134,14 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleHeaderInfoClick (e) {
|
handleHeaderInfoClick (e) {
|
||||||
const { storage } = this.props
|
const { storage, dispatch } = this.props
|
||||||
hashHistory.push('/storages/' + storage.key)
|
dispatch(push('/storages/' + storage.key))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFolderButtonClick (folderKey) {
|
handleFolderButtonClick (folderKey) {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
const { storage } = this.props
|
const { storage, dispatch } = this.props
|
||||||
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
|
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,14 +362,14 @@ class StorageItem extends React.Component {
|
|||||||
<button styleName='header-addFolderButton'
|
<button styleName='header-addFolderButton'
|
||||||
onClick={(e) => this.handleAddFolderButtonClick(e)}
|
onClick={(e) => this.handleAddFolderButtonClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-plus.svg' />
|
<img src='../resources/icon/icon-plus.svg' />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<button styleName='header-info'
|
<button styleName='header-info'
|
||||||
onClick={(e) => this.handleHeaderInfoClick(e)}
|
onClick={(e) => this.handleHeaderInfoClick(e)}
|
||||||
>
|
>
|
||||||
<span styleName='header-info-name'>
|
<span>
|
||||||
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
|
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
|
||||||
</span>
|
</span>
|
||||||
{isFolded &&
|
{isFolded &&
|
||||||
@@ -380,7 +380,7 @@ class StorageItem extends React.Component {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{this.state.isOpen &&
|
{this.state.isOpen &&
|
||||||
<div styleName='folderList' >
|
<div>
|
||||||
{folderList}
|
{folderList}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import styles from './SideNav.styl'
|
import styles from './SideNav.styl'
|
||||||
@@ -21,9 +22,10 @@ import context from 'browser/lib/context'
|
|||||||
import { remote } from 'electron'
|
import { remote } from 'electron'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
import ColorPicker from 'browser/components/ColorPicker'
|
import ColorPicker from 'browser/components/ColorPicker'
|
||||||
|
import { every, sortBy } from 'lodash'
|
||||||
|
|
||||||
function matchActiveTags (tags, activeTags) {
|
function matchActiveTags (tags, activeTags) {
|
||||||
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
return every(activeTags, v => tags.indexOf(v) >= 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SideNav extends React.Component {
|
class SideNav extends React.Component {
|
||||||
@@ -55,14 +57,14 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
deleteTag (tag) {
|
deleteTag (tag) {
|
||||||
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
ype: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Confirm tag deletion'),
|
message: i18n.__('Confirm tag deletion'),
|
||||||
detail: i18n.__('This will permanently remove this tag.'),
|
detail: i18n.__('This will permanently remove this tag.'),
|
||||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (selectedButton === 0) {
|
if (selectedButton === 0) {
|
||||||
const { data, dispatch, location, params } = this.props
|
const { data, dispatch, location, match: { params } } = this.props
|
||||||
|
|
||||||
const notes = data.noteMap
|
const notes = data.noteMap
|
||||||
.map(note => note)
|
.map(note => note)
|
||||||
@@ -92,7 +94,7 @@ class SideNav extends React.Component {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tags.splice(index, 1)
|
tags.splice(index, 1)
|
||||||
|
|
||||||
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
|
dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -104,13 +106,13 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleHomeButtonClick (e) {
|
handleHomeButtonClick (e) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/home')
|
dispatch(push('/home'))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStarredButtonClick (e) {
|
handleStarredButtonClick (e) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/starred')
|
dispatch(push('/starred'))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagContextMenu (e, tag) {
|
handleTagContextMenu (e, tag) {
|
||||||
@@ -190,18 +192,18 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTrashedButtonClick (e) {
|
handleTrashedButtonClick (e) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/trashed')
|
dispatch(push('/trashed'))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSwitchFoldersButtonClick () {
|
handleSwitchFoldersButtonClick () {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/home')
|
dispatch(push('/home'))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSwitchTagsButtonClick () {
|
handleSwitchTagsButtonClick () {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push('/alltags')
|
dispatch(push('/alltags'))
|
||||||
}
|
}
|
||||||
|
|
||||||
onSortEnd (storage) {
|
onSortEnd (storage) {
|
||||||
@@ -270,6 +272,7 @@ class SideNav extends React.Component {
|
|||||||
<div styleName='tagList'>
|
<div styleName='tagList'>
|
||||||
{this.tagListComponent(data)}
|
{this.tagListComponent(data)}
|
||||||
</div>
|
</div>
|
||||||
|
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -282,7 +285,7 @@ class SideNav extends React.Component {
|
|||||||
const { colorPicker } = this.state
|
const { colorPicker } = this.state
|
||||||
const activeTags = this.getActiveTags(location.pathname)
|
const activeTags = this.getActiveTags(location.pathname)
|
||||||
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
||||||
let tagList = _.sortBy(data.tagNoteMap.map(
|
let tagList = sortBy(data.tagNoteMap.map(
|
||||||
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
||||||
).filter(
|
).filter(
|
||||||
tag => tag.size > 0
|
tag => tag.size > 0
|
||||||
@@ -295,7 +298,7 @@ class SideNav extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (config.sortTagsBy === 'COUNTER') {
|
if (config.sortTagsBy === 'COUNTER') {
|
||||||
tagList = _.sortBy(tagList, item => (0 - item.size))
|
tagList = sortBy(tagList, item => (0 - item.size))
|
||||||
}
|
}
|
||||||
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
|
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
|
||||||
tagList = tagList.filter(
|
tagList = tagList.filter(
|
||||||
@@ -348,8 +351,8 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClickTagListItem (name) {
|
handleClickTagListItem (name) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
router.push(`/tags/${encodeURIComponent(name)}`)
|
dispatch(push(`/tags/${encodeURIComponent(name)}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortTagsByChange (e) {
|
handleSortTagsByChange (e) {
|
||||||
@@ -367,8 +370,7 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClickNarrowToTag (tag) {
|
handleClickNarrowToTag (tag) {
|
||||||
const { router } = this.context
|
const { dispatch, location } = this.props
|
||||||
const { location } = this.props
|
|
||||||
const listOfTags = this.getActiveTags(location.pathname)
|
const listOfTags = this.getActiveTags(location.pathname)
|
||||||
const indexOfTag = listOfTags.indexOf(tag)
|
const indexOfTag = listOfTags.indexOf(tag)
|
||||||
if (indexOfTag > -1) {
|
if (indexOfTag > -1) {
|
||||||
@@ -376,7 +378,7 @@ class SideNav extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
listOfTags.push(tag)
|
listOfTags.push(tag)
|
||||||
}
|
}
|
||||||
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
|
dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
emptyTrash (entries) {
|
emptyTrash (entries) {
|
||||||
@@ -440,7 +442,7 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
const style = {}
|
const style = {}
|
||||||
if (!isFolded) style.width = this.props.width
|
if (!isFolded) style.width = this.props.width
|
||||||
const isTagActive = location.pathname.match(/tag/)
|
const isTagActive = /tag/.test(location.pathname)
|
||||||
return (
|
return (
|
||||||
<div className='SideNav'
|
<div className='SideNav'
|
||||||
styleName={isFolded ? 'root--folded' : 'root'}
|
styleName={isFolded ? 'root--folded' : 'root'}
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
body[data-theme={theme}]
|
body[data-theme={theme}]
|
||||||
navButtonColor()
|
|
||||||
.zoom
|
.zoom
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
color get-theme-var(theme, 'text-color')
|
color get-theme-var(theme, 'text-color')
|
||||||
@@ -90,7 +89,7 @@ apply-theme(theme)
|
|||||||
&:active
|
&:active
|
||||||
color get-theme-var(theme, 'active-color')
|
color get-theme-var(theme, 'active-color')
|
||||||
|
|
||||||
for theme in 'dracula'
|
for theme in 'dracula' 'solarized-dark'
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|
||||||
for theme in $themes
|
for theme in $themes
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import ee from 'browser/main/lib/eventEmitter'
|
|||||||
import NewNoteButton from 'browser/main/NewNoteButton'
|
import NewNoteButton from 'browser/main/NewNoteButton'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import debounce from 'lodash/debounce'
|
import debounce from 'lodash/debounce'
|
||||||
|
import CInput from 'react-composition-input'
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
|
||||||
class TopBar extends React.Component {
|
class TopBar extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -15,26 +17,36 @@ class TopBar extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
search: '',
|
search: '',
|
||||||
searchOptions: [],
|
searchOptions: [],
|
||||||
isSearching: false,
|
isSearching: false
|
||||||
isAlphabet: false,
|
|
||||||
isIME: false,
|
|
||||||
isConfirmTranslation: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { dispatch } = this.props
|
||||||
|
|
||||||
this.focusSearchHandler = () => {
|
this.focusSearchHandler = () => {
|
||||||
this.handleOnSearchFocus()
|
this.handleOnSearchFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.codeInitHandler = this.handleCodeInit.bind(this)
|
this.codeInitHandler = this.handleCodeInit.bind(this)
|
||||||
|
this.handleKeyDown = this.handleKeyDown.bind(this)
|
||||||
|
this.handleSearchFocus = this.handleSearchFocus.bind(this)
|
||||||
|
this.handleSearchBlur = this.handleSearchBlur.bind(this)
|
||||||
|
this.handleSearchChange = this.handleSearchChange.bind(this)
|
||||||
|
this.handleSearchClearButton = this.handleSearchClearButton.bind(this)
|
||||||
|
|
||||||
this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, {
|
this.debouncedUpdateKeyword = debounce((keyword) => {
|
||||||
|
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
|
||||||
|
this.setState({
|
||||||
|
search: keyword
|
||||||
|
})
|
||||||
|
ee.emit('top:search', keyword)
|
||||||
|
}, 1000 / 60, {
|
||||||
maxWait: 1000 / 8
|
maxWait: 1000 / 8
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { params } = this.props
|
const { match: { params } } = this.props
|
||||||
const searchWord = params.searchword
|
const searchWord = params && params.searchword
|
||||||
if (searchWord !== undefined) {
|
if (searchWord !== undefined) {
|
||||||
this.setState({
|
this.setState({
|
||||||
search: searchWord,
|
search: searchWord,
|
||||||
@@ -51,22 +63,22 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSearchClearButton (e) {
|
handleSearchClearButton (e) {
|
||||||
const { router } = this.context
|
const { dispatch } = this.props
|
||||||
this.setState({
|
this.setState({
|
||||||
search: '',
|
search: '',
|
||||||
isSearching: false
|
isSearching: false
|
||||||
})
|
})
|
||||||
this.refs.search.childNodes[0].blur
|
this.refs.search.childNodes[0].blur
|
||||||
router.push('/searched')
|
dispatch(push('/searched'))
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
this.debouncedUpdateKeyword('')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
// reset states
|
// Re-apply search field on ENTER key
|
||||||
this.setState({
|
if (e.keyCode === 13) {
|
||||||
isAlphabet: false,
|
this.debouncedUpdateKeyword(e.target.value)
|
||||||
isIME: false
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// Clear search on ESC
|
// Clear search on ESC
|
||||||
if (e.keyCode === 27) {
|
if (e.keyCode === 27) {
|
||||||
@@ -84,51 +96,11 @@ class TopBar extends React.Component {
|
|||||||
ee.emit('list:prior')
|
ee.emit('list:prior')
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the key is an alphabet, del, enter or ctr
|
|
||||||
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
|
|
||||||
this.setState({
|
|
||||||
isAlphabet: true
|
|
||||||
})
|
|
||||||
// When the key is an IME input (Japanese, Chinese)
|
|
||||||
} else if (e.keyCode === 229) {
|
|
||||||
this.setState({
|
|
||||||
isIME: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyUp (e) {
|
|
||||||
// reset states
|
|
||||||
this.setState({
|
|
||||||
isConfirmTranslation: false
|
|
||||||
})
|
|
||||||
|
|
||||||
// When the key is translation confirmation (Enter, Space)
|
|
||||||
if (this.state.isIME && (e.keyCode === 32 || e.keyCode === 13)) {
|
|
||||||
this.setState({
|
|
||||||
isConfirmTranslation: true
|
|
||||||
})
|
|
||||||
const keyword = this.refs.searchInput.value
|
|
||||||
this.updateKeyword(keyword)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchChange (e) {
|
handleSearchChange (e) {
|
||||||
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
const keyword = e.target.value
|
||||||
const keyword = this.refs.searchInput.value
|
this.debouncedUpdateKeyword(keyword)
|
||||||
this.updateKeyword(keyword)
|
|
||||||
} else {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateKeyword (keyword) {
|
|
||||||
this.context.router.push(`/searched/${encodeURIComponent(keyword)}`)
|
|
||||||
this.setState({
|
|
||||||
search: keyword
|
|
||||||
})
|
|
||||||
ee.emit('top:search', keyword)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchFocus (e) {
|
handleSearchFocus (e) {
|
||||||
@@ -136,6 +108,7 @@ class TopBar extends React.Component {
|
|||||||
isSearching: true
|
isSearching: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchBlur (e) {
|
handleSearchBlur (e) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
@@ -165,7 +138,7 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleCodeInit () {
|
handleCodeInit () {
|
||||||
ee.emit('top:search', this.refs.searchInput.value)
|
ee.emit('top:search', this.refs.searchInput.value || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -178,24 +151,23 @@ class TopBar extends React.Component {
|
|||||||
<div styleName='control'>
|
<div styleName='control'>
|
||||||
<div styleName='control-search'>
|
<div styleName='control-search'>
|
||||||
<div styleName='control-search-input'
|
<div styleName='control-search-input'
|
||||||
onFocus={(e) => this.handleSearchFocus(e)}
|
onFocus={this.handleSearchFocus}
|
||||||
onBlur={(e) => this.handleSearchBlur(e)}
|
onBlur={this.handleSearchBlur}
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
ref='search'
|
ref='search'
|
||||||
>
|
>
|
||||||
<input
|
<CInput
|
||||||
ref='searchInput'
|
ref='searchInput'
|
||||||
value={this.state.search}
|
value={this.state.search}
|
||||||
onChange={(e) => this.handleSearchChange(e)}
|
onInputChange={this.handleSearchChange}
|
||||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
onKeyDown={this.handleKeyDown}
|
||||||
onKeyUp={(e) => this.handleKeyUp(e)}
|
|
||||||
placeholder={i18n.__('Search')}
|
placeholder={i18n.__('Search')}
|
||||||
type='text'
|
type='text'
|
||||||
className='searchInput'
|
className='searchInput'
|
||||||
/>
|
/>
|
||||||
{this.state.search !== '' &&
|
{this.state.search !== '' &&
|
||||||
<button styleName='control-search-input-clear'
|
<button styleName='control-search-input-clear'
|
||||||
onClick={(e) => this.handleSearchClearButton(e)}
|
onClick={this.handleSearchClearButton}
|
||||||
>
|
>
|
||||||
<i className='fa fa-fw fa-times' />
|
<i className='fa fa-fw fa-times' />
|
||||||
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
|
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
|
||||||
@@ -210,8 +182,8 @@ class TopBar extends React.Component {
|
|||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
'config',
|
'config',
|
||||||
'params',
|
'location',
|
||||||
'location'
|
'match'
|
||||||
])}
|
])}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -159,4 +159,4 @@ body[data-theme="default"]
|
|||||||
.SideNav ::-webkit-scrollbar-thumb
|
.SideNav ::-webkit-scrollbar-thumb
|
||||||
background-color rgba(255, 255, 255, 0.3)
|
background-color rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
@import '../styles/Detail/TagSelect.styl'
|
@import '../styles/Detail/TagSelect.styl'
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import Main from './Main'
|
import Main from './Main'
|
||||||
import store from './store'
|
import { store, history } from './store'
|
||||||
import React from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
require('!!style!css!stylus?sourceMap!./global.styl')
|
require('!!style!css!stylus?sourceMap!./global.styl')
|
||||||
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
|
import { Route, Switch, Redirect } from 'react-router-dom'
|
||||||
import { syncHistoryWithStore } from 'react-router-redux'
|
import { ConnectedRouter } from 'connected-react-router'
|
||||||
|
import DevTools from './DevTools'
|
||||||
|
|
||||||
require('./lib/ipcClient')
|
require('./lib/ipcClient')
|
||||||
require('../lib/customMeta')
|
require('../lib/customMeta')
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
@@ -77,7 +79,6 @@ document.addEventListener('click', function (e) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const el = document.getElementById('content')
|
const el = document.getElementById('content')
|
||||||
const history = syncHistoryWithStore(hashHistory, store)
|
|
||||||
|
|
||||||
function notify (...args) {
|
function notify (...args) {
|
||||||
return new window.Notification(...args)
|
return new window.Notification(...args)
|
||||||
@@ -98,29 +99,24 @@ function updateApp () {
|
|||||||
|
|
||||||
ReactDOM.render((
|
ReactDOM.render((
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<Route path='/' component={Main}>
|
<Fragment>
|
||||||
<IndexRedirect to='/home' />
|
<Switch>
|
||||||
<Route path='home' />
|
<Redirect path='/' to='/home' exact />
|
||||||
<Route path='starred' />
|
<Route path='/(home|alltags|starred|trashed)' component={Main} />
|
||||||
<Route path='searched'>
|
<Route path='/searched' component={Main} exact />
|
||||||
<Route path=':searchword' />
|
<Route path='/searched/:searchword' component={Main} />
|
||||||
</Route>
|
<Redirect path='/tags' to='/alltags' exact />
|
||||||
<Route path='trashed' />
|
<Route path='/tags/:tagname' component={Main} />
|
||||||
<Route path='alltags' />
|
|
||||||
<Route path='tags'>
|
{/* storages */}
|
||||||
<IndexRedirect to='/alltags' />
|
<Redirect path='/storages' to='/home' exact />
|
||||||
<Route path=':tagname' />
|
<Route path='/storages/:storageKey' component={Main} exact />
|
||||||
</Route>
|
<Route path='/storages/:storageKey/folders/:folderKey' component={Main} />
|
||||||
<Route path='storages'>
|
</Switch>
|
||||||
<IndexRedirect to='/home' />
|
<DevTools />
|
||||||
<Route path=':storageKey'>
|
</Fragment>
|
||||||
<IndexRoute />
|
</ConnectedRouter>
|
||||||
<Route path='folders/:folderKey' />
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
</Router>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
), el, function () {
|
), el, function () {
|
||||||
const loadingCover = document.getElementById('loadingCover')
|
const loadingCover = document.getElementById('loadingCover')
|
||||||
|
|||||||
@@ -9,9 +9,14 @@ const win = global.process.platform === 'win32'
|
|||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { ipcRenderer } = electron
|
const { ipcRenderer } = electron
|
||||||
const consts = require('browser/lib/consts')
|
const consts = require('browser/lib/consts')
|
||||||
|
const electronConfig = new (require('electron-config'))()
|
||||||
|
|
||||||
let isInitialized = false
|
let isInitialized = false
|
||||||
|
|
||||||
|
const DEFAULT_MARKDOWN_LINT_CONFIG = `{
|
||||||
|
"default": true
|
||||||
|
}`
|
||||||
|
|
||||||
export const DEFAULT_CONFIG = {
|
export const DEFAULT_CONFIG = {
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
isSideNavFolded: false,
|
isSideNavFolded: false,
|
||||||
@@ -23,11 +28,17 @@ export const DEFAULT_CONFIG = {
|
|||||||
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
|
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
|
||||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||||
amaEnabled: true,
|
amaEnabled: true,
|
||||||
|
autoUpdateEnabled: true,
|
||||||
hotkey: {
|
hotkey: {
|
||||||
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
||||||
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
||||||
|
toggleDirection: OSX ? 'Command + Alt + Right' : 'Ctrl + Right',
|
||||||
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
|
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
|
||||||
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
|
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
|
||||||
|
prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F',
|
||||||
|
sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S',
|
||||||
|
insertDate: OSX ? 'Command + /' : 'Ctrl + /',
|
||||||
|
insertDateTime: OSX ? 'Command + Alt + /' : 'Ctrl + Shift + /',
|
||||||
toggleMenuBar: 'Alt'
|
toggleMenuBar: 'Alt'
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
@@ -45,10 +56,11 @@ export const DEFAULT_CONFIG = {
|
|||||||
fontFamily: win ? 'Consolas' : 'Monaco',
|
fontFamily: win ? 'Consolas' : 'Monaco',
|
||||||
indentType: 'space',
|
indentType: 'space',
|
||||||
indentSize: '2',
|
indentSize: '2',
|
||||||
|
lineWrapping: true,
|
||||||
enableRulers: false,
|
enableRulers: false,
|
||||||
rulers: [80, 120],
|
rulers: [80, 120],
|
||||||
displayLineNumbers: true,
|
displayLineNumbers: true,
|
||||||
matchingPairs: '()[]{}\'\'""$$**``',
|
matchingPairs: '()[]{}\'\'""$$**``~~__',
|
||||||
matchingTriples: '```"""\'\'\'',
|
matchingTriples: '```"""\'\'\'',
|
||||||
explodingPairs: '[]{}``$$',
|
explodingPairs: '[]{}``$$',
|
||||||
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
||||||
@@ -60,7 +72,16 @@ export const DEFAULT_CONFIG = {
|
|||||||
enableFrontMatterTitle: true,
|
enableFrontMatterTitle: true,
|
||||||
frontMatterTitleField: 'title',
|
frontMatterTitleField: 'title',
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
enableSmartPaste: false
|
enableSmartPaste: false,
|
||||||
|
enableMarkdownLint: false,
|
||||||
|
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
|
||||||
|
prettierConfig: ` {
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}`,
|
||||||
|
deleteUnusedAttachments: true
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
fontSize: '14',
|
fontSize: '14',
|
||||||
@@ -78,8 +99,10 @@ export const DEFAULT_CONFIG = {
|
|||||||
breaks: true,
|
breaks: true,
|
||||||
smartArrows: false,
|
smartArrows: false,
|
||||||
allowCustomCSS: false,
|
allowCustomCSS: false,
|
||||||
customCSS: '',
|
|
||||||
|
customCSS: '/* Drop Your Custom CSS Code Here */',
|
||||||
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||||
|
mermaidHTMLLabel: false,
|
||||||
lineThroughCheckbox: true
|
lineThroughCheckbox: true
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
@@ -103,7 +126,6 @@ function validate (config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _save (config) {
|
function _save (config) {
|
||||||
console.log(config)
|
|
||||||
window.localStorage.setItem('config', JSON.stringify(config))
|
window.localStorage.setItem('config', JSON.stringify(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +145,8 @@ function get () {
|
|||||||
_save(config)
|
_save(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.autoUpdateEnabled = electronConfig.get('autoUpdateEnabled', config.autoUpdateEnabled)
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
let editorTheme = document.getElementById('editorTheme')
|
let editorTheme = document.getElementById('editorTheme')
|
||||||
@@ -133,16 +157,12 @@ function get () {
|
|||||||
document.head.appendChild(editorTheme)
|
document.head.appendChild(editorTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
const theme = consts.THEMES.find(theme => theme.name === config.editor.theme)
|
||||||
? config.editor.theme
|
|
||||||
: 'default'
|
|
||||||
|
|
||||||
if (config.editor.theme !== 'default') {
|
if (theme) {
|
||||||
if (config.editor.theme.startsWith('solarized')) {
|
editorTheme.setAttribute('href', theme.path)
|
||||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
|
} else {
|
||||||
} else {
|
config.editor.theme = 'default'
|
||||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +171,13 @@ function get () {
|
|||||||
|
|
||||||
function set (updates) {
|
function set (updates) {
|
||||||
const currentConfig = get()
|
const currentConfig = get()
|
||||||
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
|
|
||||||
|
const arrangedUpdates = updates
|
||||||
|
if (updates.preview !== undefined && updates.preview.customCSS === '') {
|
||||||
|
arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, arrangedUpdates)
|
||||||
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
|
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
|
||||||
_save(newConfig)
|
_save(newConfig)
|
||||||
|
|
||||||
@@ -170,18 +196,15 @@ function set (updates) {
|
|||||||
editorTheme.setAttribute('rel', 'stylesheet')
|
editorTheme.setAttribute('rel', 'stylesheet')
|
||||||
document.head.appendChild(editorTheme)
|
document.head.appendChild(editorTheme)
|
||||||
}
|
}
|
||||||
const newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
|
|
||||||
? newConfig.editor.theme
|
|
||||||
: 'default'
|
|
||||||
|
|
||||||
if (newTheme !== 'default') {
|
const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme)
|
||||||
if (newTheme.startsWith('solarized')) {
|
|
||||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
|
if (newTheme) {
|
||||||
} else {
|
editorTheme.setAttribute('href', newTheme.path)
|
||||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
electronConfig.set('autoUpdateEnabled', newConfig.autoUpdateEnabled)
|
||||||
|
|
||||||
ipcRenderer.send('config-renew', {
|
ipcRenderer.send('config-renew', {
|
||||||
config: get()
|
config: get()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const escapeStringRegexp = require('escape-string-regexp')
|
|||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
const url = require('url')
|
const url = require('url')
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import { isString } from 'lodash'
|
||||||
|
|
||||||
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
||||||
const DESTINATION_FOLDER = 'attachments'
|
const DESTINATION_FOLDER = 'attachments'
|
||||||
@@ -19,7 +20,7 @@ const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(
|
|||||||
* @returns {Promise<Image>} Image element created
|
* @returns {Promise<Image>} Image element created
|
||||||
*/
|
*/
|
||||||
function getImage (file) {
|
function getImage (file) {
|
||||||
if (_.isString(file)) {
|
if (isString(file)) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.onload = () => resolve(img)
|
img.onload = () => resolve(img)
|
||||||
@@ -85,7 +86,7 @@ function getOrientation (file) {
|
|||||||
return view.getUint16(offset + (i * 12) + 8, little)
|
return view.getUint16(offset + (i * 12) + 8, little)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
|
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker.
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
offset += view.getUint16(offset, false)
|
offset += view.getUint16(offset, false)
|
||||||
@@ -241,6 +242,10 @@ function migrateAttachments (markdownContent, storagePath, noteKey) {
|
|||||||
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
|
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
|
||||||
*/
|
*/
|
||||||
function fixLocalURLS (renderedHTML, storagePath) {
|
function fixLocalURLS (renderedHTML, storagePath) {
|
||||||
|
const encodedWin32SeparatorRegex = /%5C/g
|
||||||
|
const storageRegex = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g')
|
||||||
|
const storageUrl = 'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
|
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
|
||||||
|
|
||||||
@@ -250,8 +255,7 @@ function fixLocalURLS (renderedHTML, storagePath) {
|
|||||||
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
|
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
|
||||||
*/
|
*/
|
||||||
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
|
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
|
||||||
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
|
return match.replace(encodedWin32SeparatorRegex, '/').replace(storageRegex, storageUrl)
|
||||||
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,27 +282,40 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
|||||||
let promise
|
let promise
|
||||||
if (dropEvent.dataTransfer.files.length > 0) {
|
if (dropEvent.dataTransfer.files.length > 0) {
|
||||||
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
|
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
|
||||||
if (file.type.startsWith('image')) {
|
const filePath = file.path
|
||||||
if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
|
const fileType = file.type // EX) 'image/gif' or 'text/html'
|
||||||
return copyAttachment(file.path, storageKey, noteKey).then(fileName => ({
|
if (fileType.startsWith('image')) {
|
||||||
|
if (fileType === 'image/gif' || fileType === 'image/svg+xml') {
|
||||||
|
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
|
||||||
fileName,
|
fileName,
|
||||||
title: path.basename(file.path),
|
title: path.basename(filePath),
|
||||||
isImage: true
|
isImage: true
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
return fixRotate(file)
|
return getOrientation(file)
|
||||||
.then(data => copyAttachment({type: 'base64', data: data, sourceFilePath: file.path}, storageKey, noteKey)
|
.then((orientation) => {
|
||||||
.then(fileName => ({
|
if (orientation === -1) { // The image rotation is correct and does not need adjustment
|
||||||
|
return copyAttachment(filePath, storageKey, noteKey)
|
||||||
|
} else {
|
||||||
|
return fixRotate(file).then(data => copyAttachment({
|
||||||
|
type: 'base64',
|
||||||
|
data: data,
|
||||||
|
sourceFilePath: filePath
|
||||||
|
}, storageKey, noteKey))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(fileName =>
|
||||||
|
({
|
||||||
fileName,
|
fileName,
|
||||||
title: path.basename(file.path),
|
title: path.basename(filePath),
|
||||||
isImage: true
|
isImage: true
|
||||||
}))
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return copyAttachment(file.path, storageKey, noteKey).then(fileName => ({
|
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
|
||||||
fileName,
|
fileName,
|
||||||
title: path.basename(file.path),
|
title: path.basename(filePath),
|
||||||
isImage: false
|
isImage: false
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -325,13 +342,18 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
|||||||
canvas.height = image.height
|
canvas.height = image.height
|
||||||
context.drawImage(image, 0, 0)
|
context.drawImage(image, 0, 0)
|
||||||
|
|
||||||
return copyAttachment({type: 'base64', data: canvas.toDataURL(), sourceFilePath: imageURL}, storageKey, noteKey)
|
return copyAttachment({
|
||||||
|
type: 'base64',
|
||||||
|
data: canvas.toDataURL(),
|
||||||
|
sourceFilePath: imageURL
|
||||||
|
}, storageKey, noteKey)
|
||||||
})
|
})
|
||||||
.then(fileName => ({
|
.then(fileName => ({
|
||||||
fileName,
|
fileName,
|
||||||
title: imageURL,
|
title: imageURL,
|
||||||
isImage: true
|
isImage: true
|
||||||
}))])
|
}))
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.then(files => {
|
promise.then(files => {
|
||||||
@@ -449,6 +471,54 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Copies the attachments to the storage folder and returns the mardown content it should be replaced with
|
||||||
|
* @param {String} markDownContent content in which the attachment paths should be found
|
||||||
|
* @param {String} filepath The path of the file with attachments to import
|
||||||
|
* @param {String} storageKey Storage key of the destination storage
|
||||||
|
* @param {String} noteKey Key of the current note. Will be used as subfolder in :storage
|
||||||
|
*/
|
||||||
|
function importAttachments (markDownContent, filepath, storageKey, noteKey) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const nameRegex = /(!\[.*?]\()(.+?\..+?)(\))/g
|
||||||
|
let attachPath = nameRegex.exec(markDownContent)
|
||||||
|
const promiseArray = []
|
||||||
|
const attachmentPaths = []
|
||||||
|
const groupIndex = 2
|
||||||
|
|
||||||
|
while (attachPath) {
|
||||||
|
let attachmentPath = attachPath[groupIndex]
|
||||||
|
attachmentPaths.push(attachmentPath)
|
||||||
|
attachmentPath = path.isAbsolute(attachmentPath) ? attachmentPath : path.join(path.dirname(filepath), attachmentPath)
|
||||||
|
promiseArray.push(this.copyAttachment(attachmentPath, storageKey, noteKey))
|
||||||
|
attachPath = nameRegex.exec(markDownContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
let numResolvedPromises = 0
|
||||||
|
|
||||||
|
if (promiseArray.length === 0) {
|
||||||
|
resolve(markDownContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < promiseArray.length; j++) {
|
||||||
|
promiseArray[j]
|
||||||
|
.then((fileName) => {
|
||||||
|
const newPath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName)
|
||||||
|
markDownContent = markDownContent.replace(attachmentPaths[j], newPath)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('File does not exist in path: ' + attachmentPaths[j])
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
numResolvedPromises++
|
||||||
|
if (numResolvedPromises === promiseArray.length) {
|
||||||
|
resolve(markDownContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Moves the attachments of the current note to the new location.
|
* @description Moves the attachments of the current note to the new location.
|
||||||
* Returns a modified version of the given content so that the links to the attachments point to the new note key.
|
* Returns a modified version of the given content so that the links to the attachments point to the new note key.
|
||||||
@@ -551,11 +621,79 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
console.info('Attachment folder ("' + attachmentFolder + '") did not exist..')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Get all existing attachments related to a specific note
|
||||||
|
including their status (in use or not) and their path. Return null if there're no attachment related to note or specified parametters are invalid
|
||||||
|
* @param markdownContent markdownContent of the current note
|
||||||
|
* @param storageKey StorageKey of the current note
|
||||||
|
* @param noteKey NoteKey of the currentNote
|
||||||
|
* @return {Promise<Array<{path: String, isInUse: bool}>>} Promise returning the
|
||||||
|
list of attachments with their properties */
|
||||||
|
function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) {
|
||||||
|
if (storageKey == null || noteKey == null || markdownContent == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
|
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
|
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
|
||||||
|
const attachmentsInNoteOnlyFileNames = []
|
||||||
|
if (attachmentsInNote) {
|
||||||
|
for (let i = 0; i < attachmentsInNote.length; i++) {
|
||||||
|
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fs.existsSync(attachmentFolder)) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.readdir(attachmentFolder, (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error reading directory "' + attachmentFolder + '". Error:')
|
||||||
|
console.error(err)
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const attachments = []
|
||||||
|
for (const file of files) {
|
||||||
|
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
|
||||||
|
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
|
||||||
|
attachments.push({ path: absolutePathOfFile, isInUse: false })
|
||||||
|
} else {
|
||||||
|
attachments.push({ path: absolutePathOfFile, isInUse: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(attachments)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Remove all specified attachment paths
|
||||||
|
* @param attachments attachment paths
|
||||||
|
* @return {Promise} Promise after all attachments are removed */
|
||||||
|
function removeAttachmentsByPaths (attachments) {
|
||||||
|
const promises = []
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
fs.unlink(attachment, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Could not delete "%s"', attachment)
|
||||||
|
console.error(err)
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
promises.push(promise)
|
||||||
|
}
|
||||||
|
return Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones the attachments of a given note.
|
* Clones the attachments of a given note.
|
||||||
* Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination.
|
* Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination.
|
||||||
@@ -656,9 +794,12 @@ module.exports = {
|
|||||||
handlePasteNativeImage,
|
handlePasteNativeImage,
|
||||||
getAttachmentsInMarkdownContent,
|
getAttachmentsInMarkdownContent,
|
||||||
getAbsolutePathsOfAttachmentsInContent,
|
getAbsolutePathsOfAttachmentsInContent,
|
||||||
|
importAttachments,
|
||||||
removeStorageAndNoteReferences,
|
removeStorageAndNoteReferences,
|
||||||
|
removeAttachmentsByPaths,
|
||||||
deleteAttachmentFolder,
|
deleteAttachmentFolder,
|
||||||
deleteAttachmentsNotPresentInNote,
|
deleteAttachmentsNotPresentInNote,
|
||||||
|
getAttachmentsPathAndStatus,
|
||||||
moveAttachments,
|
moveAttachments,
|
||||||
cloneAttachments,
|
cloneAttachments,
|
||||||
isAttachmentLink,
|
isAttachmentLink,
|
||||||
|
|||||||
86
browser/main/lib/dataApi/createNoteFromUrl.js
Normal file
86
browser/main/lib/dataApi/createNoteFromUrl.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
const http = require('http')
|
||||||
|
const https = require('https')
|
||||||
|
const { createTurndownService } = require('../../../lib/turndown')
|
||||||
|
const createNote = require('./createNote')
|
||||||
|
|
||||||
|
import { push } from 'connected-react-router'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
|
||||||
|
function validateUrl (str) {
|
||||||
|
if (/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(str)) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR_MESSAGES = {
|
||||||
|
ENOTFOUND: 'URL not found. Please check the URL, or your internet connection and try again.',
|
||||||
|
VALIDATION_ERROR: 'Please check if the URL follows this format: https://www.google.com',
|
||||||
|
UNEXPECTED: 'Unexpected error! Please check console for details!'
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const td = createTurndownService()
|
||||||
|
|
||||||
|
if (!validateUrl(url)) {
|
||||||
|
reject({result: false, error: ERROR_MESSAGES.VALIDATION_ERROR})
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = url.startsWith('https') ? https : http
|
||||||
|
|
||||||
|
const req = request.request(url, (res) => {
|
||||||
|
let data = ''
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk
|
||||||
|
})
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
const markdownHTML = td.turndown(data)
|
||||||
|
|
||||||
|
if (dispatch !== null) {
|
||||||
|
createNote(storage, {
|
||||||
|
type: 'MARKDOWN_NOTE',
|
||||||
|
folder: folder,
|
||||||
|
title: '',
|
||||||
|
content: markdownHTML
|
||||||
|
})
|
||||||
|
.then((note) => {
|
||||||
|
const noteHash = note.key
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
dispatch(push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {key: noteHash}
|
||||||
|
}))
|
||||||
|
ee.emit('list:jump', noteHash)
|
||||||
|
ee.emit('detail:focus')
|
||||||
|
resolve({result: true, error: null})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
createNote(storage, {
|
||||||
|
type: 'MARKDOWN_NOTE',
|
||||||
|
folder: folder,
|
||||||
|
title: '',
|
||||||
|
content: markdownHTML
|
||||||
|
}).then((note) => {
|
||||||
|
resolve({result: true, note, error: null})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
req.on('error', (e) => {
|
||||||
|
console.error('error in parsing URL', e)
|
||||||
|
reject({result: false, error: ERROR_MESSAGES[e.code] || ERROR_MESSAGES.UNEXPECTED})
|
||||||
|
})
|
||||||
|
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createNoteFromUrl
|
||||||
@@ -3,7 +3,6 @@ const path = require('path')
|
|||||||
const resolveStorageData = require('./resolveStorageData')
|
const resolveStorageData = require('./resolveStorageData')
|
||||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
const sander = require('sander')
|
|
||||||
const { findStorage } = require('browser/lib/findStorage')
|
const { findStorage } = require('browser/lib/findStorage')
|
||||||
const deleteSingleNote = require('./deleteNote')
|
const deleteSingleNote = require('./deleteNote')
|
||||||
|
|
||||||
|
|||||||
@@ -43,19 +43,18 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
|||||||
.then(function exportNotes (data) {
|
.then(function exportNotes (data) {
|
||||||
const { storage, notes } = data
|
const { storage, notes } = data
|
||||||
|
|
||||||
notes
|
return Promise.all(notes
|
||||||
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
||||||
.forEach(note => {
|
.map(note => {
|
||||||
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
|
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
|
||||||
exportNote(note.key, storage.path, note.content, notePath, null)
|
return exportNote(note.key, storage.path, note.content, notePath, null)
|
||||||
})
|
})
|
||||||
|
).then(() => ({
|
||||||
return {
|
|
||||||
storage,
|
storage,
|
||||||
folderKey,
|
folderKey,
|
||||||
fileType,
|
fileType,
|
||||||
exportDir
|
exportDir
|
||||||
}
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,14 +43,17 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (outputFormatter) {
|
if (outputFormatter) {
|
||||||
exportedData = outputFormatter(exportedData, exportTasks)
|
exportedData = outputFormatter(exportedData, exportTasks, targetPath)
|
||||||
|
} else {
|
||||||
|
exportedData = Promise.resolve(exportedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
||||||
|
|
||||||
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
|
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
|
||||||
.then(() => {
|
.then(() => exportedData)
|
||||||
return saveToFile(exportedData, targetPath)
|
.then(data => {
|
||||||
|
return saveToFile(data, targetPath)
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
rollbackExport(tasks)
|
rollbackExport(tasks)
|
||||||
throw err
|
throw err
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const dataApi = {
|
|||||||
exportFolder: require('./exportFolder'),
|
exportFolder: require('./exportFolder'),
|
||||||
exportStorage: require('./exportStorage'),
|
exportStorage: require('./exportStorage'),
|
||||||
createNote: require('./createNote'),
|
createNote: require('./createNote'),
|
||||||
|
createNoteFromUrl: require('./createNoteFromUrl'),
|
||||||
updateNote: require('./updateNote'),
|
updateNote: require('./updateNote'),
|
||||||
deleteNote: require('./deleteNote'),
|
deleteNote: require('./deleteNote'),
|
||||||
moveNote: require('./moveNote'),
|
moveNote: require('./moveNote'),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const resolveStorageData = require('./resolveStorageData')
|
const resolveStorageData = require('./resolveStorageData')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
|
||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
const keygen = require('browser/lib/keygen')
|
const keygen = require('browser/lib/keygen')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import store from '../store'
|
import { store } from '../store'
|
||||||
|
|
||||||
class ModalBase extends React.Component {
|
class ModalBase extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ module.exports = {
|
|||||||
'toggleMode': () => {
|
'toggleMode': () => {
|
||||||
ee.emit('topbar:togglemodebutton')
|
ee.emit('topbar:togglemodebutton')
|
||||||
},
|
},
|
||||||
|
'toggleDirection': () => {
|
||||||
|
ee.emit('topbar:toggledirectionbutton')
|
||||||
|
},
|
||||||
'deleteNote': () => {
|
'deleteNote': () => {
|
||||||
ee.emit('hotkey:deletenote')
|
ee.emit('hotkey:deletenote')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './CreateFolderModal.styl'
|
import styles from './CreateFolderModal.styl'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import store from 'browser/main/store'
|
import { store } from 'browser/main/store'
|
||||||
import consts from 'browser/lib/consts'
|
import consts from 'browser/lib/consts'
|
||||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
|||||||
118
browser/main/modals/CreateMarkdownFromURLModal.js
Normal file
118
browser/main/modals/CreateMarkdownFromURLModal.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './CreateMarkdownFromURLModal.styl'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
class CreateMarkdownFromURLModal extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
name: '',
|
||||||
|
showerror: false,
|
||||||
|
errormessage: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.refs.name.focus()
|
||||||
|
this.refs.name.select()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseButtonClick (e) {
|
||||||
|
this.props.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange (e) {
|
||||||
|
this.setState({
|
||||||
|
name: this.refs.name.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown (e) {
|
||||||
|
if (e.keyCode === 27) {
|
||||||
|
this.props.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputKeyDown (e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 13:
|
||||||
|
this.confirm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfirmButtonClick (e) {
|
||||||
|
this.confirm()
|
||||||
|
}
|
||||||
|
|
||||||
|
showError (message) {
|
||||||
|
this.setState({
|
||||||
|
showerror: true,
|
||||||
|
errormessage: message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hideError () {
|
||||||
|
this.setState({
|
||||||
|
showerror: false,
|
||||||
|
errormessage: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm () {
|
||||||
|
this.hideError()
|
||||||
|
const { storage, folder, dispatch, location } = this.props
|
||||||
|
|
||||||
|
dataApi.createNoteFromUrl(this.state.name, storage, folder, dispatch, location).then((result) => {
|
||||||
|
this.props.close()
|
||||||
|
}).catch((result) => {
|
||||||
|
this.showError(result.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div styleName='root'
|
||||||
|
tabIndex='-1'
|
||||||
|
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||||
|
>
|
||||||
|
<div styleName='header'>
|
||||||
|
<div styleName='title'>{i18n.__('Import Markdown From URL')}</div>
|
||||||
|
</div>
|
||||||
|
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />
|
||||||
|
<div styleName='control'>
|
||||||
|
<div styleName='control-folder'>
|
||||||
|
<div styleName='control-folder-label'>{i18n.__('Insert URL Here')}</div>
|
||||||
|
<input styleName='control-folder-input'
|
||||||
|
ref='name'
|
||||||
|
value={this.state.name}
|
||||||
|
onChange={(e) => this.handleChange(e)}
|
||||||
|
onKeyDown={(e) => this.handleInputKeyDown(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button styleName='control-confirmButton'
|
||||||
|
onClick={(e) => this.handleConfirmButtonClick(e)}
|
||||||
|
>
|
||||||
|
{i18n.__('Import')}
|
||||||
|
</button>
|
||||||
|
<div className='error' styleName='error'>{this.state.errormessage}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateMarkdownFromURLModal.propTypes = {
|
||||||
|
storage: PropTypes.string,
|
||||||
|
folder: PropTypes.string,
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
location: PropTypes.shape({
|
||||||
|
pathname: PropTypes.string
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(CreateMarkdownFromURLModal, styles)
|
||||||
160
browser/main/modals/CreateMarkdownFromURLModal.styl
Normal file
160
browser/main/modals/CreateMarkdownFromURLModal.styl
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
.root
|
||||||
|
modal()
|
||||||
|
width 500px
|
||||||
|
height 270px
|
||||||
|
overflow hidden
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.header
|
||||||
|
height 80px
|
||||||
|
margin-bottom 10px
|
||||||
|
margin-top 20px
|
||||||
|
font-size 18px
|
||||||
|
line-height 50px
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
|
.title
|
||||||
|
font-size 36px
|
||||||
|
font-weight 600
|
||||||
|
|
||||||
|
.control-folder-label
|
||||||
|
text-align left
|
||||||
|
font-size 14px
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
|
.control-folder-input
|
||||||
|
display block
|
||||||
|
height 40px
|
||||||
|
width 490px
|
||||||
|
padding 0 5px
|
||||||
|
margin 10px 0
|
||||||
|
border 1px solid $ui-input--create-folder-modal
|
||||||
|
border-radius 2px
|
||||||
|
background-color transparent
|
||||||
|
outline none
|
||||||
|
vertical-align middle
|
||||||
|
font-size 16px
|
||||||
|
&:disabled
|
||||||
|
background-color $ui-input--disabled-backgroundColor
|
||||||
|
&:focus, &:active
|
||||||
|
border-color $ui-active-color
|
||||||
|
|
||||||
|
.control-confirmButton
|
||||||
|
display block
|
||||||
|
height 35px
|
||||||
|
width 140px
|
||||||
|
border none
|
||||||
|
border-radius 2px
|
||||||
|
padding 0 25px
|
||||||
|
margin 20px auto
|
||||||
|
font-size 14px
|
||||||
|
colorPrimaryButton()
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
modalDark()
|
||||||
|
width 500px
|
||||||
|
height 270px
|
||||||
|
overflow hidden
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.header
|
||||||
|
background-color transparent
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.control-folder-label
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.control-folder-input
|
||||||
|
border 1px solid $ui-input--create-folder-modal
|
||||||
|
color white
|
||||||
|
|
||||||
|
.description
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.control-confirmButton
|
||||||
|
colorDarkPrimaryButton()
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
modalSolarizedDark()
|
||||||
|
width 500px
|
||||||
|
height 270px
|
||||||
|
overflow hidden
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.header
|
||||||
|
background-color transparent
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.control-folder-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.control-folder-input
|
||||||
|
border 1px solid $ui-input--create-folder-modal
|
||||||
|
color white
|
||||||
|
|
||||||
|
.description
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.control-confirmButton
|
||||||
|
colorSolarizedDarkPrimaryButton()
|
||||||
|
|
||||||
|
.error
|
||||||
|
text-align center
|
||||||
|
color #F44336
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.root
|
||||||
|
modalMonokai()
|
||||||
|
width 500px
|
||||||
|
height 270px
|
||||||
|
overflow hidden
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.header
|
||||||
|
background-color transparent
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
.control-folder-label
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
.control-folder-input
|
||||||
|
border 1px solid $ui-input--create-folder-modal
|
||||||
|
color white
|
||||||
|
|
||||||
|
.description
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.control-confirmButton
|
||||||
|
colorMonokaiPrimaryButton()
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
modalDracula()
|
||||||
|
width 500px
|
||||||
|
height 270px
|
||||||
|
overflow hidden
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.header
|
||||||
|
background-color transparent
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.control-folder-label
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.control-folder-input
|
||||||
|
border 1px solid $ui-input--create-folder-modal
|
||||||
|
color white
|
||||||
|
|
||||||
|
.description
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.control-confirmButton
|
||||||
|
colorDraculaPrimaryButton()
|
||||||
@@ -3,12 +3,15 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './NewNoteModal.styl'
|
import styles from './NewNoteModal.styl'
|
||||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import { openModal } from 'browser/main/lib/modal'
|
||||||
|
import CreateMarkdownFromURLModal from '../modals/CreateMarkdownFromURLModal'
|
||||||
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
|
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
|
||||||
|
import queryString from 'query-string'
|
||||||
|
|
||||||
class NewNoteModal extends React.Component {
|
class NewNoteModal extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
this.lock = false
|
||||||
this.state = {}
|
this.state = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,13 +23,29 @@ class NewNoteModal extends React.Component {
|
|||||||
this.props.close()
|
this.props.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMarkdownNoteButtonClick (e) {
|
handleCreateMarkdownFromUrlClick (e) {
|
||||||
const { storage, folder, dispatch, location, params, config } = this.props
|
this.props.close()
|
||||||
createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => {
|
|
||||||
setTimeout(this.props.close, 200)
|
const { storage, folder, dispatch, location } = this.props
|
||||||
|
openModal(CreateMarkdownFromURLModal, {
|
||||||
|
storage: storage,
|
||||||
|
folder: folder,
|
||||||
|
dispatch,
|
||||||
|
location
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMarkdownNoteButtonClick (e) {
|
||||||
|
const { storage, folder, dispatch, location, config } = this.props
|
||||||
|
const params = location.search !== '' && queryString.parse(location.search)
|
||||||
|
if (!this.lock) {
|
||||||
|
this.lock = true
|
||||||
|
createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||||
|
setTimeout(this.props.close, 200)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleMarkdownNoteButtonKeyDown (e) {
|
handleMarkdownNoteButtonKeyDown (e) {
|
||||||
if (e.keyCode === 9) {
|
if (e.keyCode === 9) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -35,10 +54,14 @@ class NewNoteModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSnippetNoteButtonClick (e) {
|
handleSnippetNoteButtonClick (e) {
|
||||||
const { storage, folder, dispatch, location, params, config } = this.props
|
const { storage, folder, dispatch, location, config } = this.props
|
||||||
createSnippetNote(storage, folder, dispatch, location, params, config).then(() => {
|
const params = location.search !== '' && queryString.parse(location.search)
|
||||||
setTimeout(this.props.close, 200)
|
if (!this.lock) {
|
||||||
})
|
this.lock = true
|
||||||
|
createSnippetNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||||
|
setTimeout(this.props.close, 200)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSnippetNoteButtonKeyDown (e) {
|
handleSnippetNoteButtonKeyDown (e) {
|
||||||
@@ -106,10 +129,8 @@ class NewNoteModal extends React.Component {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div styleName='description'>
|
<div styleName='description'><i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')}</div>
|
||||||
<i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')}
|
<div styleName='from-url' onClick={(e) => this.handleCreateMarkdownFromUrlClick(e)}>Or, create a new markdown note from a URL</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
.control
|
.control
|
||||||
padding 25px 0px
|
padding 25px 0px
|
||||||
text-align center
|
text-align center
|
||||||
|
display: flex
|
||||||
|
|
||||||
.control-button
|
.control-button
|
||||||
width 240px
|
width 240px
|
||||||
@@ -47,6 +48,12 @@
|
|||||||
text-align center
|
text-align center
|
||||||
margin-bottom 25px
|
margin-bottom 25px
|
||||||
|
|
||||||
|
.from-url
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
text-align center
|
||||||
|
margin-bottom 25px
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
body[data-theme={theme}]
|
body[data-theme={theme}]
|
||||||
.root
|
.root
|
||||||
@@ -69,4 +76,4 @@ for theme in 'dark' 'solarized-dark' 'dracula'
|
|||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|
||||||
for theme in $themes
|
for theme in $themes
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './ConfigTab.styl'
|
import styles from './ConfigTab.styl'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import store from 'browser/main/store'
|
import { store } from 'browser/main/store'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|||||||
@@ -18,6 +18,14 @@
|
|||||||
margin-bottom 15px
|
margin-bottom 15px
|
||||||
margin-top 30px
|
margin-top 30px
|
||||||
|
|
||||||
|
.group-header--sub
|
||||||
|
@extend .group-header
|
||||||
|
margin-bottom 10px
|
||||||
|
|
||||||
|
.group-header2--sub
|
||||||
|
@extend .group-header2
|
||||||
|
margin-bottom 10px
|
||||||
|
|
||||||
.group-section
|
.group-section
|
||||||
margin-bottom 20px
|
margin-bottom 20px
|
||||||
display flex
|
display flex
|
||||||
@@ -138,10 +146,12 @@ body[data-theme="dark"]
|
|||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.group-header
|
.group-header
|
||||||
|
.group-header--sub
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
.group-header2
|
.group-header2
|
||||||
|
.group-header2--sub
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.group-section-control-input
|
.group-section-control-input
|
||||||
@@ -192,4 +202,4 @@ for theme in 'solarized-dark' 'dracula'
|
|||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|
||||||
for theme in $themes
|
for theme in $themes
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|||||||
@@ -22,18 +22,16 @@ class Crowdfunding extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div styleName='root'>
|
<div styleName='root'>
|
||||||
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
|
<div styleName='group-header'>{i18n.__('Crowdfunding')}</div>
|
||||||
<p>{i18n.__('Thank you for using Boostnote!')}</p>
|
<p>{i18n.__('Thank you for using Boostnote!')}</p>
|
||||||
<br />
|
<br />
|
||||||
<p>{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}</p>
|
<p>{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}</p>
|
||||||
<p>{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}</p>
|
<p>{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}</p>
|
||||||
<br />
|
<div styleName='group-header2--sub'>{i18n.__('Sustainable Open Source Ecosystem')}</div>
|
||||||
<p>{i18n.__('### Sustainable Open Source Ecosystem')}</p>
|
|
||||||
<p>{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}</p>
|
<p>{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}</p>
|
||||||
<p>{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}</p>
|
<p>{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}</p>
|
||||||
<p>{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}</p>
|
<p>{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}</p>
|
||||||
<br />
|
<div styleName='group-header2--sub'>{i18n.__('We believe Meritocracy')}</div>
|
||||||
<p>{i18n.__('### We believe Meritocracy')}</p>
|
|
||||||
<p>{i18n.__('We think developers who have skills and do great things must be rewarded properly.')}</p>
|
<p>{i18n.__('We think developers who have skills and do great things must be rewarded properly.')}</p>
|
||||||
<p>{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}</p>
|
<p>{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}</p>
|
||||||
<p>{i18n.__('It sometimes looks like exploitation.')}</p>
|
<p>{i18n.__('It sometimes looks like exploitation.')}</p>
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
@import('./Tab')
|
@import('./ConfigTab')
|
||||||
|
|
||||||
.root
|
|
||||||
padding 15px
|
|
||||||
white-space pre
|
|
||||||
line-height 1.4
|
|
||||||
color alpha($ui-text-color, 90%)
|
|
||||||
width 100%
|
|
||||||
font-size 14px
|
|
||||||
p
|
p
|
||||||
font-size 16px
|
font-size 16px
|
||||||
|
line-height 1.4
|
||||||
|
|
||||||
.cf-link
|
.cf-link
|
||||||
height 35px
|
height 35px
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import styles from './FolderItem.styl'
|
import styles from './FolderItem.styl'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import store from 'browser/main/store'
|
import { store } from 'browser/main/store'
|
||||||
import { SketchPicker } from 'react-color'
|
import { SketchPicker } from 'react-color'
|
||||||
import { SortableElement, SortableHandle } from 'react-sortable-hoc'
|
import { SortableElement, SortableHandle } from 'react-sortable-hoc'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
@@ -225,7 +225,7 @@ class FolderItem extends React.Component {
|
|||||||
<div styleName='folderItem-left'
|
<div styleName='folderItem-left'
|
||||||
style={{borderColor: folder.color}}
|
style={{borderColor: folder.color}}
|
||||||
>
|
>
|
||||||
<span styleName='folderItem-left-name'>{folder.name}</span>
|
<span>{folder.name}</span>
|
||||||
<span styleName='folderItem-left-key'>({folder.key})</span>
|
<span styleName='folderItem-left-key'>({folder.key})</span>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='folderItem-right'>
|
<div styleName='folderItem-right'>
|
||||||
@@ -288,10 +288,10 @@ class Handle extends React.Component {
|
|||||||
|
|
||||||
class SortableFolderItemComponent extends React.Component {
|
class SortableFolderItemComponent extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const StyledHandle = CSSModules(Handle, this.props.styles)
|
const StyledHandle = CSSModules(Handle, styles)
|
||||||
const DragHandle = SortableHandle(StyledHandle)
|
const DragHandle = SortableHandle(StyledHandle)
|
||||||
|
|
||||||
const StyledFolderItem = CSSModules(FolderItem, this.props.styles)
|
const StyledFolderItem = CSSModules(FolderItem, styles)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import styles from './FolderList.styl'
|
import styles from './FolderList.styl'
|
||||||
import store from 'browser/main/store'
|
import { store } from 'browser/main/store'
|
||||||
import FolderItem from './FolderItem'
|
import FolderItem from './FolderItem'
|
||||||
import { SortableContainer } from 'react-sortable-hoc'
|
import { SortableContainer } from 'react-sortable-hoc'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
@@ -22,7 +22,7 @@ class FolderList extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div styleName='folderList'>
|
<div>
|
||||||
{folderList.length > 0
|
{folderList.length > 0
|
||||||
? folderList
|
? folderList
|
||||||
: <div styleName='folderList-empty'>{i18n.__('No Folders')}</div>
|
: <div styleName='folderList-empty'>{i18n.__('No Folders')}</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './ConfigTab.styl'
|
import styles from './ConfigTab.styl'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import store from 'browser/main/store'
|
import { store } from 'browser/main/store'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
@@ -30,7 +30,8 @@ class HotkeyTab extends React.Component {
|
|||||||
this.handleSettingError = (err) => {
|
this.handleSettingError = (err) => {
|
||||||
if (
|
if (
|
||||||
this.state.config.hotkey.toggleMain === '' ||
|
this.state.config.hotkey.toggleMain === '' ||
|
||||||
this.state.config.hotkey.toggleMode === ''
|
this.state.config.hotkey.toggleMode === '' ||
|
||||||
|
this.state.config.hotkey.toggleDirection === ''
|
||||||
) {
|
) {
|
||||||
this.setState({keymapAlert: {
|
this.setState({keymapAlert: {
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -76,13 +77,17 @@ class HotkeyTab extends React.Component {
|
|||||||
|
|
||||||
handleHotkeyChange (e) {
|
handleHotkeyChange (e) {
|
||||||
const { config } = this.state
|
const { config } = this.state
|
||||||
config.hotkey = {
|
config.hotkey = Object.assign({}, config.hotkey, {
|
||||||
toggleMain: this.refs.toggleMain.value,
|
toggleMain: this.refs.toggleMain.value,
|
||||||
toggleMode: this.refs.toggleMode.value,
|
toggleMode: this.refs.toggleMode.value,
|
||||||
|
toggleDirection: this.refs.toggleDirection.value,
|
||||||
deleteNote: this.refs.deleteNote.value,
|
deleteNote: this.refs.deleteNote.value,
|
||||||
pasteSmartly: this.refs.pasteSmartly.value,
|
pasteSmartly: this.refs.pasteSmartly.value,
|
||||||
toggleMenuBar: this.refs.toggleMenuBar.value
|
prettifyMarkdown: this.refs.prettifyMarkdown.value,
|
||||||
}
|
toggleMenuBar: this.refs.toggleMenuBar.value,
|
||||||
|
insertDate: this.refs.insertDate.value,
|
||||||
|
insertDateTime: this.refs.insertDateTime.value
|
||||||
|
})
|
||||||
this.setState({
|
this.setState({
|
||||||
config
|
config
|
||||||
})
|
})
|
||||||
@@ -151,6 +156,17 @@ class HotkeyTab extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>{i18n.__('Toggle Direction')}</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
onChange={(e) => this.handleHotkeyChange(e)}
|
||||||
|
ref='toggleDirection'
|
||||||
|
value={config.hotkey.toggleDirection}
|
||||||
|
type='text'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div styleName='group-section'>
|
<div styleName='group-section'>
|
||||||
<div styleName='group-section-label'>{i18n.__('Delete Note')}</div>
|
<div styleName='group-section-label'>{i18n.__('Delete Note')}</div>
|
||||||
<div styleName='group-section-control'>
|
<div styleName='group-section-control'>
|
||||||
@@ -173,6 +189,38 @@ class HotkeyTab extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>{i18n.__('Prettify Markdown')}</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
onChange={(e) => this.handleHotkeyChange(e)}
|
||||||
|
ref='prettifyMarkdown'
|
||||||
|
value={config.hotkey.prettifyMarkdown}
|
||||||
|
type='text' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>{i18n.__('Insert Current Date')}</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
ref='insertDate'
|
||||||
|
value={config.hotkey.insertDate}
|
||||||
|
type='text'
|
||||||
|
disabled='true'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>{i18n.__('Insert Current Date and Time')}</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
ref='insertDateTime'
|
||||||
|
value={config.hotkey.insertDateTime}
|
||||||
|
type='text'
|
||||||
|
disabled='true'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div styleName='group-control'>
|
<div styleName='group-control'>
|
||||||
<button styleName='group-control-leftButton'
|
<button styleName='group-control-leftButton'
|
||||||
onClick={(e) => this.handleHintToggleButtonClick(e)}
|
onClick={(e) => this.handleHintToggleButtonClick(e)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './InfoTab.styl'
|
import styles from './InfoTab.styl'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import store from 'browser/main/store'
|
import { store } from 'browser/main/store'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
@@ -61,6 +61,15 @@ class InfoTab extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleAutoUpdate () {
|
||||||
|
const newConfig = {
|
||||||
|
autoUpdateEnabled: !this.state.config.autoUpdateEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ config: newConfig })
|
||||||
|
ConfigManager.set(newConfig)
|
||||||
|
}
|
||||||
|
|
||||||
infoMessage () {
|
infoMessage () {
|
||||||
const { amaMessage } = this.state
|
const { amaMessage } = this.state
|
||||||
return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null
|
return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null
|
||||||
@@ -69,8 +78,7 @@ class InfoTab extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div styleName='root'>
|
<div styleName='root'>
|
||||||
|
<div styleName='group-header'>{i18n.__('Community')}</div>
|
||||||
<div styleName='header--sub'>{i18n.__('Community')}</div>
|
|
||||||
<div styleName='top'>
|
<div styleName='top'>
|
||||||
<ul styleName='list'>
|
<ul styleName='list'>
|
||||||
<li>
|
<li>
|
||||||
@@ -108,7 +116,7 @@ class InfoTab extends React.Component {
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div styleName='header--sub'>{i18n.__('About')}</div>
|
<div styleName='group-header--sub'>{i18n.__('About')}</div>
|
||||||
|
|
||||||
<div styleName='top'>
|
<div styleName='top'>
|
||||||
<div styleName='icon-space'>
|
<div styleName='icon-space'>
|
||||||
@@ -134,16 +142,18 @@ class InfoTab extends React.Component {
|
|||||||
>{i18n.__('Development')}</a>{i18n.__(' : Development configurations for Boostnote.')}
|
>{i18n.__('Development')}</a>{i18n.__(' : Development configurations for Boostnote.')}
|
||||||
</li>
|
</li>
|
||||||
<li styleName='cc'>
|
<li styleName='cc'>
|
||||||
{i18n.__('Copyright (C) 2017 - 2019 BoostIO')}
|
{i18n.__('Copyright (C) 2017 - 2020 BoostIO')}
|
||||||
</li>
|
</li>
|
||||||
<li styleName='cc'>
|
<li styleName='cc'>
|
||||||
{i18n.__('License: GPL v3')}
|
{i18n.__('License: GPL v3')}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div><label><input type='checkbox' onChange={this.toggleAutoUpdate.bind(this)} checked={this.state.config.autoUpdateEnabled} />{i18n.__('Enable Auto Update')}</label></div>
|
||||||
|
|
||||||
<hr styleName='separate-line' />
|
<hr styleName='separate-line' />
|
||||||
|
|
||||||
<div styleName='policy'>{i18n.__('Analytics')}</div>
|
<div styleName='group-header2--sub'>{i18n.__('Analytics')}</div>
|
||||||
<div>{i18n.__('Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.')}</div>
|
<div>{i18n.__('Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.')}</div>
|
||||||
<div>{i18n.__('You can see how it works on ')}<a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
|
<div>{i18n.__('You can see how it works on ')}<a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -1,16 +1,4 @@
|
|||||||
@import('./Tab')
|
@import('./ConfigTab.styl')
|
||||||
|
|
||||||
.root
|
|
||||||
padding 15px
|
|
||||||
white-space pre
|
|
||||||
line-height 1.4
|
|
||||||
color alpha($ui-text-color, 90%)
|
|
||||||
width 100%
|
|
||||||
font-size 14px
|
|
||||||
|
|
||||||
.top
|
|
||||||
text-align left
|
|
||||||
margin-bottom 20px
|
|
||||||
|
|
||||||
.icon-space
|
.icon-space
|
||||||
margin 20px 0
|
margin 20px 0
|
||||||
@@ -45,13 +33,21 @@
|
|||||||
.separate-line
|
.separate-line
|
||||||
margin 40px 0
|
margin 40px 0
|
||||||
|
|
||||||
.policy
|
|
||||||
width 100%
|
|
||||||
font-size 20px
|
|
||||||
margin-bottom 10px
|
|
||||||
|
|
||||||
.policy-submit
|
.policy-submit
|
||||||
margin-top 10px
|
margin-top 10px
|
||||||
|
height 35px
|
||||||
|
border-radius 2px
|
||||||
|
border none
|
||||||
|
background-color alpha(#1EC38B, 90%)
|
||||||
|
padding-left 20px
|
||||||
|
padding-right 20px
|
||||||
|
text-decoration none
|
||||||
|
color white
|
||||||
|
font-weight 600
|
||||||
|
font-size 16px
|
||||||
|
&:hover
|
||||||
|
background-color #1EC38B
|
||||||
|
transition 0.2s
|
||||||
|
|
||||||
.policy-confirm
|
.policy-confirm
|
||||||
margin-top 10px
|
margin-top 10px
|
||||||
@@ -60,11 +56,16 @@
|
|||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
color alpha($tab--dark-text-color, 80%)
|
color alpha($tab--dark-text-color, 80%)
|
||||||
|
.appId
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
body[data-theme={theme}]
|
body[data-theme={theme}]
|
||||||
.root
|
.root
|
||||||
color get-theme-var(theme, 'text-color')
|
color get-theme-var(theme, 'text-color')
|
||||||
|
.appId
|
||||||
|
color get-theme-var(theme, 'text-color')
|
||||||
.list
|
.list
|
||||||
a
|
a
|
||||||
color get-theme-var(theme, 'active-color')
|
color get-theme-var(theme, 'active-color')
|
||||||
@@ -73,4 +74,4 @@ for theme in 'solarized-dark' 'dracula'
|
|||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|
||||||
for theme in $themes
|
for theme in $themes
|
||||||
apply-theme(theme)
|
apply-theme(theme)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import _ from 'lodash'
|
|||||||
import styles from './SnippetTab.styl'
|
import styles from './SnippetTab.styl'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import snippetManager from '../../../lib/SnippetManager'
|
||||||
|
|
||||||
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
const buildCMRulers = (rulers, enableRulers) =>
|
const buildCMRulers = (rulers, enableRulers) =>
|
||||||
@@ -64,7 +65,9 @@ class SnippetEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveSnippet () {
|
saveSnippet () {
|
||||||
dataApi.updateSnippet(this.snippet).catch((err) => { throw err })
|
dataApi.updateSnippet(this.snippet)
|
||||||
|
.then(snippets => snippetManager.assignSnippets(snippets))
|
||||||
|
.catch((err) => { throw err })
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class SnippetTab extends React.Component {
|
|||||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
return (
|
return (
|
||||||
<div styleName='root'>
|
<div styleName='root'>
|
||||||
<div styleName='header'>{i18n.__('Snippets')}</div>
|
<div styleName='group-header'>{i18n.__('Snippets')}</div>
|
||||||
<SnippetList
|
<SnippetList
|
||||||
onSnippetSelect={this.handleSnippetSelect.bind(this)}
|
onSnippetSelect={this.handleSnippetSelect.bind(this)}
|
||||||
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
|
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
@import('./Tab')
|
|
||||||
@import('./ConfigTab')
|
@import('./ConfigTab')
|
||||||
|
|
||||||
.root
|
|
||||||
padding 15px
|
|
||||||
white-space pre
|
|
||||||
line-height 1.4
|
|
||||||
color alpha($ui-text-color, 90%)
|
|
||||||
width 100%
|
|
||||||
font-size 14px
|
|
||||||
|
|
||||||
.group
|
.group
|
||||||
margin-bottom 45px
|
margin-bottom 45px
|
||||||
|
|
||||||
@@ -127,7 +118,7 @@
|
|||||||
background darken(#f5f5f5, 5)
|
background darken(#f5f5f5, 5)
|
||||||
|
|
||||||
.snippet-detail
|
.snippet-detail
|
||||||
width 70%
|
width 67%
|
||||||
height calc(100% - 200px)
|
height calc(100% - 200px)
|
||||||
position absolute
|
position absolute
|
||||||
left 33%
|
left 33%
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './StorageItem.styl'
|
import styles from './StorageItem.styl'
|
||||||
import consts from 'browser/lib/consts'
|
import consts from 'browser/lib/consts'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import store from 'browser/main/store'
|
import { store } from 'browser/main/store'
|
||||||
import FolderList from './FolderList'
|
import FolderList from './FolderList'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
|||||||
@@ -101,4 +101,19 @@ body[data-theme="solarized-dark"]
|
|||||||
.header-control-button
|
.header-control-button
|
||||||
border-color $ui-solarized-dark-button-backgroundColor
|
border-color $ui-solarized-dark-button-backgroundColor
|
||||||
background-color $ui-solarized-dark-button-backgroundColor
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
apply-theme(theme)
|
||||||
|
body[data-theme={theme}]
|
||||||
|
.header
|
||||||
|
border-color get-theme-var(theme, 'borderColor')
|
||||||
|
|
||||||
|
.header-control-button
|
||||||
|
colorThemedPrimaryButton(theme)
|
||||||
|
border-color get-theme-var(theme, 'borderColor')
|
||||||
|
|
||||||
|
for theme in 'dracula'
|
||||||
|
apply-theme(theme)
|
||||||
|
|
||||||
|
for theme in $themes
|
||||||
|
apply-theme(theme)
|
||||||
@@ -3,8 +3,11 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './StoragesTab.styl'
|
import styles from './StoragesTab.styl'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||||
import StorageItem from './StorageItem'
|
import StorageItem from './StorageItem'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import { humanFileSize } from 'browser/lib/utils'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { shell, remote } = electron
|
const { shell, remote } = electron
|
||||||
@@ -35,8 +38,29 @@ class StoragesTab extends React.Component {
|
|||||||
name: 'Unnamed',
|
name: 'Unnamed',
|
||||||
type: 'FILESYSTEM',
|
type: 'FILESYSTEM',
|
||||||
path: ''
|
path: ''
|
||||||
}
|
},
|
||||||
|
attachments: []
|
||||||
}
|
}
|
||||||
|
this.loadAttachmentStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAttachmentStorage () {
|
||||||
|
const promises = []
|
||||||
|
this.props.data.noteMap.map(note => {
|
||||||
|
const promise = attachmentManagement.getAttachmentsPathAndStatus(
|
||||||
|
note.content,
|
||||||
|
note.storage,
|
||||||
|
note.key
|
||||||
|
)
|
||||||
|
if (promise) promises.push(promise)
|
||||||
|
})
|
||||||
|
|
||||||
|
Promise.all(promises)
|
||||||
|
.then(data => {
|
||||||
|
const result = data.reduce((acc, curr) => acc.concat(curr), [])
|
||||||
|
this.setState({attachments: result})
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAddStorageButton (e) {
|
handleAddStorageButton (e) {
|
||||||
@@ -57,8 +81,39 @@ class StoragesTab extends React.Component {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRemoveUnusedAttachments (attachments) {
|
||||||
|
attachmentManagement.removeAttachmentsByPaths(attachments)
|
||||||
|
.then(() => this.loadAttachmentStorage())
|
||||||
|
.catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
renderList () {
|
renderList () {
|
||||||
const { data, boundingBox } = this.props
|
const { data, boundingBox } = this.props
|
||||||
|
const { attachments } = this.state
|
||||||
|
|
||||||
|
const unusedAttachments = attachments.filter(attachment => !attachment.isInUse)
|
||||||
|
const inUseAttachments = attachments.filter(attachment => attachment.isInUse)
|
||||||
|
|
||||||
|
const totalUnusedAttachments = unusedAttachments.length
|
||||||
|
const totalInuseAttachments = inUseAttachments.length
|
||||||
|
const totalAttachments = totalUnusedAttachments + totalInuseAttachments
|
||||||
|
|
||||||
|
const totalUnusedAttachmentsSize = unusedAttachments
|
||||||
|
.reduce((acc, curr) => {
|
||||||
|
const stats = fs.statSync(curr.path)
|
||||||
|
const fileSizeInBytes = stats.size
|
||||||
|
return acc + fileSizeInBytes
|
||||||
|
}, 0)
|
||||||
|
const totalInuseAttachmentsSize = inUseAttachments
|
||||||
|
.reduce((acc, curr) => {
|
||||||
|
const stats = fs.statSync(curr.path)
|
||||||
|
const fileSizeInBytes = stats.size
|
||||||
|
return acc + fileSizeInBytes
|
||||||
|
}, 0)
|
||||||
|
const totalAttachmentsSize = totalUnusedAttachmentsSize + totalInuseAttachmentsSize
|
||||||
|
|
||||||
|
const unusedAttachmentPaths = unusedAttachments
|
||||||
|
.reduce((acc, curr) => acc.concat(curr.path), [])
|
||||||
|
|
||||||
if (!boundingBox) { return null }
|
if (!boundingBox) { return null }
|
||||||
const storageList = data.storageMap.map((storage) => {
|
const storageList = data.storageMap.map((storage) => {
|
||||||
@@ -82,6 +137,20 @@ class StoragesTab extends React.Component {
|
|||||||
<i className='fa fa-plus' /> {i18n.__('Add Storage Location')}
|
<i className='fa fa-plus' /> {i18n.__('Add Storage Location')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div styleName='header'>{i18n.__('Attachment storage')}</div>
|
||||||
|
<p styleName='list-attachment-label'>
|
||||||
|
Unused attachments size: {humanFileSize(totalUnusedAttachmentsSize)} ({totalUnusedAttachments} items)
|
||||||
|
</p>
|
||||||
|
<p styleName='list-attachment-label'>
|
||||||
|
In use attachments size: {humanFileSize(totalInuseAttachmentsSize)} ({totalInuseAttachments} items)
|
||||||
|
</p>
|
||||||
|
<p styleName='list-attachment-label'>
|
||||||
|
Total attachments size: {humanFileSize(totalAttachmentsSize)} ({totalAttachments} items)
|
||||||
|
</p>
|
||||||
|
<button styleName='list-attachement-clear-button'
|
||||||
|
onClick={() => this.handleRemoveUnusedAttachments(unusedAttachmentPaths)}>
|
||||||
|
{i18n.__('Clear unused attachments')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user