mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 09:46:22 +00:00
Merge branch 'master' into drop-browser-image
This commit is contained in:
@@ -2,10 +2,15 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import _ from 'lodash'
|
||||
import CodeMirror from 'codemirror'
|
||||
import hljs from 'highlight.js'
|
||||
import 'codemirror-mode-elixir'
|
||||
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import { options, TableEditor, Alignment } from '@susisu/mte-kernel'
|
||||
import {
|
||||
options,
|
||||
TableEditor,
|
||||
Alignment
|
||||
} from '@susisu/mte-kernel'
|
||||
import TextEditorInterface from 'browser/lib/TextEditorInterface'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import iconv from 'iconv-lite'
|
||||
@@ -13,17 +18,104 @@ import crypto from 'crypto'
|
||||
import consts from 'browser/lib/consts'
|
||||
import styles from '../components/CodeEditor.styl'
|
||||
import fs from 'fs'
|
||||
const { ipcRenderer, remote } = require('electron')
|
||||
const { ipcRenderer, remote, clipboard } = require('electron')
|
||||
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||
const spellcheck = require('browser/lib/spellcheck')
|
||||
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
|
||||
import TurndownService from 'turndown'
|
||||
import { gfm } from 'turndown-plugin-gfm'
|
||||
import {
|
||||
gfm
|
||||
} from 'turndown-plugin-gfm'
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
const buildCMRulers = (rulers, enableRulers) =>
|
||||
(enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
|
||||
(enableRulers ? rulers.map(ruler => ({
|
||||
column: ruler
|
||||
})) : [])
|
||||
|
||||
function translateHotkey (hotkey) {
|
||||
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 {
|
||||
constructor (props) {
|
||||
@@ -34,6 +126,7 @@ export default class CodeEditor extends React.Component {
|
||||
trailing: true
|
||||
})
|
||||
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
|
||||
this.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject)
|
||||
this.focusHandler = () => {
|
||||
ipcRenderer.send('editor:focused', true)
|
||||
}
|
||||
@@ -49,14 +142,21 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
|
||||
const { storageKey, noteKey } = this.props
|
||||
const {
|
||||
storageKey,
|
||||
noteKey
|
||||
} = this.props
|
||||
attachmentManagement.deleteAttachmentsNotPresentInNote(
|
||||
this.editor.getValue(),
|
||||
storageKey,
|
||||
noteKey
|
||||
)
|
||||
}
|
||||
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
|
||||
this.pasteHandler = (editor, e) => {
|
||||
e.preventDefault()
|
||||
|
||||
this.handlePaste(editor, false)
|
||||
}
|
||||
this.loadStyleHandler = e => {
|
||||
this.editor.refresh()
|
||||
}
|
||||
@@ -65,12 +165,16 @@ export default class CodeEditor extends React.Component {
|
||||
this.scrollToLineHandeler = this.scrollToLine.bind(this)
|
||||
|
||||
this.formatTable = () => this.handleFormatTable()
|
||||
this.contextMenuHandler = function (editor, event) {
|
||||
const menu = buildEditorContextMenu(editor, event)
|
||||
if (menu != null) {
|
||||
setTimeout(() => menu.popup(remote.getCurrentWindow()), 30)
|
||||
|
||||
if (props.switchPreview !== 'RIGHTCLICK') {
|
||||
this.contextMenuHandler = function (editor, event) {
|
||||
const menu = buildEditorContextMenu(editor, event)
|
||||
if (menu != null) {
|
||||
setTimeout(() => menu.popup(remote.getCurrentWindow()), 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.editorActivityHandler = () => this.handleEditorActivity()
|
||||
|
||||
this.turndownService = new TurndownService()
|
||||
@@ -111,7 +215,9 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
handleFormatTable () {
|
||||
this.tableEditor.formatAll(options({textWidthOptions: {}}))
|
||||
this.tableEditor.formatAll(options({
|
||||
textWidthOptions: {}
|
||||
}))
|
||||
}
|
||||
|
||||
handleEditorActivity () {
|
||||
@@ -120,42 +226,9 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
updateTableEditorState () {
|
||||
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
|
||||
if (active) {
|
||||
if (this.extraKeysMode !== 'editor') {
|
||||
this.extraKeysMode = 'editor'
|
||||
this.editor.setOption('extraKeys', this.editorKeyMap)
|
||||
}
|
||||
} else {
|
||||
if (this.extraKeysMode !== 'default') {
|
||||
this.extraKeysMode = 'default'
|
||||
this.editor.setOption('extraKeys', this.defaultKeyMap)
|
||||
this.tableEditor.resetSmartCursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { rulers, enableRulers } = this.props
|
||||
updateDefaultKeyMap () {
|
||||
const { hotkey } = this.props
|
||||
const expandSnippet = this.expandSnippet.bind(this)
|
||||
eventEmitter.on('line:jump', this.scrollToLineHandeler)
|
||||
|
||||
const defaultSnippet = [
|
||||
{
|
||||
id: crypto.randomBytes(16).toString('hex'),
|
||||
name: 'Dummy text',
|
||||
prefix: ['lorem', 'ipsum'],
|
||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||
}
|
||||
]
|
||||
if (!fs.existsSync(consts.SNIPPET_FILE)) {
|
||||
fs.writeFileSync(
|
||||
consts.SNIPPET_FILE,
|
||||
JSON.stringify(defaultSnippet, null, 4),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
|
||||
Tab: function (cm) {
|
||||
@@ -198,6 +271,9 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-Left': function (cm) {
|
||||
cm.execCommand('goLineLeft')
|
||||
},
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
@@ -207,13 +283,56 @@ export default class CodeEditor extends React.Component {
|
||||
document.execCommand('copy')
|
||||
}
|
||||
return CodeMirror.Pass
|
||||
},
|
||||
[translateHotkey(hotkey.pasteSmartly)]: cm => {
|
||||
this.handlePaste(cm, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateTableEditorState () {
|
||||
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
|
||||
if (active) {
|
||||
if (this.extraKeysMode !== 'editor') {
|
||||
this.extraKeysMode = 'editor'
|
||||
this.editor.setOption('extraKeys', this.editorKeyMap)
|
||||
}
|
||||
} else {
|
||||
if (this.extraKeysMode !== 'default') {
|
||||
this.extraKeysMode = 'default'
|
||||
this.editor.setOption('extraKeys', this.defaultKeyMap)
|
||||
this.tableEditor.resetSmartCursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { rulers, enableRulers } = this.props
|
||||
eventEmitter.on('line:jump', this.scrollToLineHandeler)
|
||||
|
||||
const defaultSnippet = [
|
||||
{
|
||||
id: crypto.randomBytes(16).toString('hex'),
|
||||
name: 'Dummy text',
|
||||
prefix: ['lorem', 'ipsum'],
|
||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||
}
|
||||
]
|
||||
if (!fs.existsSync(consts.SNIPPET_FILE)) {
|
||||
fs.writeFileSync(
|
||||
consts.SNIPPET_FILE,
|
||||
JSON.stringify(defaultSnippet, null, 4),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
this.updateDefaultKeyMap()
|
||||
|
||||
this.value = this.props.value
|
||||
this.editor = CodeMirror(this.refs.root, {
|
||||
rulers: buildCMRulers(rulers, enableRulers),
|
||||
value: this.props.value,
|
||||
linesHighlighted: this.props.linesHighlighted,
|
||||
lineNumbers: this.props.displayLineNumbers,
|
||||
lineWrapping: true,
|
||||
theme: this.props.theme,
|
||||
@@ -227,21 +346,28 @@ export default class CodeEditor extends React.Component {
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseBrackets: {
|
||||
pairs: '()[]{}\'\'""$$**``',
|
||||
triples: '```"""\'\'\'',
|
||||
explode: '[]{}``$$',
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
},
|
||||
extraKeys: this.defaultKeyMap
|
||||
})
|
||||
|
||||
this.setMode(this.props.mode)
|
||||
if (!this.props.mode && this.props.value && this.props.autoDetect) {
|
||||
this.autoDetectLanguage(this.props.value)
|
||||
} else {
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
|
||||
this.editor.on('focus', this.focusHandler)
|
||||
this.editor.on('blur', this.blurHandler)
|
||||
this.editor.on('change', this.changeHandler)
|
||||
this.editor.on('gutterClick', this.highlightHandler)
|
||||
this.editor.on('paste', this.pasteHandler)
|
||||
this.editor.on('contextmenu', this.contextMenuHandler)
|
||||
if (this.props.switchPreview !== 'RIGHTCLICK') {
|
||||
this.editor.on('contextmenu', this.contextMenuHandler)
|
||||
}
|
||||
eventEmitter.on('top:search', this.searchHandler)
|
||||
|
||||
eventEmitter.emit('code:init')
|
||||
@@ -269,43 +395,117 @@ export default class CodeEditor extends React.Component {
|
||||
})
|
||||
|
||||
this.editorKeyMap = CodeMirror.normalizeKeyMap({
|
||||
'Tab': () => { this.tableEditor.nextCell(this.tableEditorOptions) },
|
||||
'Shift-Tab': () => { this.tableEditor.previousCell(this.tableEditorOptions) },
|
||||
'Enter': () => { this.tableEditor.nextRow(this.tableEditorOptions) },
|
||||
'Ctrl-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
|
||||
'Cmd-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
|
||||
'Ctrl-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
|
||||
'Cmd-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
|
||||
'Ctrl-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
|
||||
'Cmd-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
|
||||
'Ctrl-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
|
||||
'Cmd-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
|
||||
'Ctrl-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
|
||||
'Cmd-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
|
||||
'Ctrl-K Ctrl-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
|
||||
'Cmd-K Cmd-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
|
||||
'Ctrl-L Ctrl-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
|
||||
'Cmd-L Cmd-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
|
||||
'Ctrl-K Ctrl-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
|
||||
'Cmd-K Cmd-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
|
||||
'Ctrl-L Ctrl-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
|
||||
'Cmd-L Cmd-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) }
|
||||
'Tab': () => {
|
||||
this.tableEditor.nextCell(this.tableEditorOptions)
|
||||
},
|
||||
'Shift-Tab': () => {
|
||||
this.tableEditor.previousCell(this.tableEditorOptions)
|
||||
},
|
||||
'Enter': () => {
|
||||
this.tableEditor.nextRow(this.tableEditorOptions)
|
||||
},
|
||||
'Ctrl-Enter': () => {
|
||||
this.tableEditor.escape(this.tableEditorOptions)
|
||||
},
|
||||
'Cmd-Enter': () => {
|
||||
this.tableEditor.escape(this.tableEditorOptions)
|
||||
},
|
||||
'Shift-Ctrl-Left': () => {
|
||||
this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions)
|
||||
},
|
||||
'Shift-Cmd-Left': () => {
|
||||
this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions)
|
||||
},
|
||||
'Shift-Ctrl-Right': () => {
|
||||
this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions)
|
||||
},
|
||||
'Shift-Cmd-Right': () => {
|
||||
this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions)
|
||||
},
|
||||
'Shift-Ctrl-Up': () => {
|
||||
this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions)
|
||||
},
|
||||
'Shift-Cmd-Up': () => {
|
||||
this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions)
|
||||
},
|
||||
'Shift-Ctrl-Down': () => {
|
||||
this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions)
|
||||
},
|
||||
'Shift-Cmd-Down': () => {
|
||||
this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions)
|
||||
},
|
||||
'Ctrl-Left': () => {
|
||||
this.tableEditor.moveFocus(0, -1, this.tableEditorOptions)
|
||||
},
|
||||
'Cmd-Left': () => {
|
||||
this.tableEditor.moveFocus(0, -1, this.tableEditorOptions)
|
||||
},
|
||||
'Ctrl-Right': () => {
|
||||
this.tableEditor.moveFocus(0, 1, this.tableEditorOptions)
|
||||
},
|
||||
'Cmd-Right': () => {
|
||||
this.tableEditor.moveFocus(0, 1, this.tableEditorOptions)
|
||||
},
|
||||
'Ctrl-Up': () => {
|
||||
this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions)
|
||||
},
|
||||
'Cmd-Up': () => {
|
||||
this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions)
|
||||
},
|
||||
'Ctrl-Down': () => {
|
||||
this.tableEditor.moveFocus(1, 0, this.tableEditorOptions)
|
||||
},
|
||||
'Cmd-Down': () => {
|
||||
this.tableEditor.moveFocus(1, 0, this.tableEditorOptions)
|
||||
},
|
||||
'Ctrl-K Ctrl-I': () => {
|
||||
this.tableEditor.insertRow(this.tableEditorOptions)
|
||||
},
|
||||
'Cmd-K Cmd-I': () => {
|
||||
this.tableEditor.insertRow(this.tableEditorOptions)
|
||||
},
|
||||
'Ctrl-L Ctrl-I': () => {
|
||||
this.tableEditor.deleteRow(this.tableEditorOptions)
|
||||
},
|
||||
'Cmd-L Cmd-I': () => {
|
||||
this.tableEditor.deleteRow(this.tableEditorOptions)
|
||||
},
|
||||
'Ctrl-K Ctrl-J': () => {
|
||||
this.tableEditor.insertColumn(this.tableEditorOptions)
|
||||
},
|
||||
'Cmd-K Cmd-J': () => {
|
||||
this.tableEditor.insertColumn(this.tableEditorOptions)
|
||||
},
|
||||
'Ctrl-L Ctrl-J': () => {
|
||||
this.tableEditor.deleteColumn(this.tableEditorOptions)
|
||||
},
|
||||
'Cmd-L Cmd-J': () => {
|
||||
this.tableEditor.deleteColumn(this.tableEditorOptions)
|
||||
},
|
||||
'Alt-Shift-Ctrl-Left': () => {
|
||||
this.tableEditor.moveColumn(-1, this.tableEditorOptions)
|
||||
},
|
||||
'Alt-Shift-Cmd-Left': () => {
|
||||
this.tableEditor.moveColumn(-1, this.tableEditorOptions)
|
||||
},
|
||||
'Alt-Shift-Ctrl-Right': () => {
|
||||
this.tableEditor.moveColumn(1, this.tableEditorOptions)
|
||||
},
|
||||
'Alt-Shift-Cmd-Right': () => {
|
||||
this.tableEditor.moveColumn(1, this.tableEditorOptions)
|
||||
},
|
||||
'Alt-Shift-Ctrl-Up': () => {
|
||||
this.tableEditor.moveRow(-1, this.tableEditorOptions)
|
||||
},
|
||||
'Alt-Shift-Cmd-Up': () => {
|
||||
this.tableEditor.moveRow(-1, this.tableEditorOptions)
|
||||
},
|
||||
'Alt-Shift-Ctrl-Down': () => {
|
||||
this.tableEditor.moveRow(1, this.tableEditorOptions)
|
||||
},
|
||||
'Alt-Shift-Cmd-Down': () => {
|
||||
this.tableEditor.moveRow(1, this.tableEditorOptions)
|
||||
}
|
||||
})
|
||||
|
||||
if (this.props.enableTableEditor) {
|
||||
@@ -316,6 +516,8 @@ export default class CodeEditor extends React.Component {
|
||||
this.setState({
|
||||
clientWidth: this.refs.root.clientWidth
|
||||
})
|
||||
|
||||
this.initialHighlighting()
|
||||
}
|
||||
|
||||
expandSnippet (line, cursor, cm, snippets) {
|
||||
@@ -392,8 +594,14 @@ export default class CodeEditor extends React.Component {
|
||||
return {
|
||||
text: wordBeforeCursor,
|
||||
range: {
|
||||
from: { line: lineNumber, ch: originCursorPosition },
|
||||
to: { line: lineNumber, ch: cursorPosition }
|
||||
from: {
|
||||
line: lineNumber,
|
||||
ch: originCursorPosition
|
||||
},
|
||||
to: {
|
||||
line: lineNumber,
|
||||
ch: cursorPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,7 +627,10 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
let needRefresh = false
|
||||
const { rulers, enableRulers } = this.props
|
||||
const {
|
||||
rulers,
|
||||
enableRulers
|
||||
} = this.props
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
@@ -460,6 +671,18 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
|
||||
}
|
||||
|
||||
if (prevProps.matchingPairs !== this.props.matchingPairs ||
|
||||
prevProps.matchingTriples !== this.props.matchingTriples ||
|
||||
prevProps.explodingPairs !== this.props.explodingPairs) {
|
||||
const bracketObject = {
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
}
|
||||
this.editor.setOption('autoCloseBrackets', bracketObject)
|
||||
}
|
||||
|
||||
if (prevProps.enableTableEditor !== this.props.enableTableEditor) {
|
||||
if (this.props.enableTableEditor) {
|
||||
this.editor.on('cursorActivity', this.editorActivityHandler)
|
||||
@@ -473,6 +696,14 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setOption('extraKeys', this.defaultKeyMap)
|
||||
}
|
||||
|
||||
if (prevProps.hotkey !== this.props.hotkey) {
|
||||
this.updateDefaultKeyMap()
|
||||
|
||||
if (this.extraKeysMode === 'default') {
|
||||
this.editor.setOption('extraKeys', this.defaultKeyMap)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.clientWidth !== this.refs.root.clientWidth) {
|
||||
this.setState({
|
||||
clientWidth: this.refs.root.clientWidth
|
||||
@@ -484,7 +715,7 @@ export default class CodeEditor extends React.Component {
|
||||
if (prevProps.spellCheck !== this.props.spellCheck) {
|
||||
if (this.props.spellCheck === false) {
|
||||
spellcheck.setLanguage(this.editor, spellcheck.SPELLCHECK_DISABLED)
|
||||
let elem = document.getElementById('editor-bottom-panel')
|
||||
const elem = document.getElementById('editor-bottom-panel')
|
||||
elem.parentNode.removeChild(elem)
|
||||
} else {
|
||||
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
|
||||
@@ -497,7 +728,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
setMode (mode) {
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(mode))
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text'))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
this.editor.setOption('mode', syntax.mime)
|
||||
@@ -506,12 +737,96 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
handleChange (editor, changeObject) {
|
||||
spellcheck.handleChange(editor, changeObject)
|
||||
|
||||
this.updateHighlight(editor, changeObject)
|
||||
|
||||
this.value = editor.getValue()
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(editor)
|
||||
}
|
||||
}
|
||||
|
||||
incrementLines (start, linesAdded, linesRemoved, editor) {
|
||||
let highlightedLines = editor.options.linesHighlighted
|
||||
|
||||
const totalHighlightedLines = highlightedLines.length
|
||||
|
||||
let offset = linesAdded - linesRemoved
|
||||
|
||||
// Store new items to be added as we're changing the lines
|
||||
let newLines = []
|
||||
|
||||
let i = totalHighlightedLines
|
||||
|
||||
while (i--) {
|
||||
const lineNumber = highlightedLines[i]
|
||||
|
||||
// Interval that will need to be updated
|
||||
// Between start and (start + offset) remove highlight
|
||||
if (lineNumber >= start) {
|
||||
highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1)
|
||||
|
||||
// Lines that need to be relocated
|
||||
if (lineNumber >= (start + linesRemoved)) {
|
||||
newLines.push(lineNumber + offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding relocated lines
|
||||
highlightedLines.push(...newLines)
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(editor)
|
||||
}
|
||||
}
|
||||
|
||||
handleHighlight (editor, changeObject) {
|
||||
const lines = editor.options.linesHighlighted
|
||||
|
||||
if (!lines.includes(changeObject)) {
|
||||
lines.push(changeObject)
|
||||
editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
|
||||
} else {
|
||||
lines.splice(lines.indexOf(changeObject), 1)
|
||||
editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
|
||||
}
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(editor)
|
||||
}
|
||||
}
|
||||
|
||||
updateHighlight (editor, changeObject) {
|
||||
const linesAdded = changeObject.text.length - 1
|
||||
const linesRemoved = changeObject.removed.length - 1
|
||||
|
||||
// If no lines added or removed return
|
||||
if (linesAdded === 0 && linesRemoved === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let start = changeObject.from.line
|
||||
|
||||
switch (changeObject.origin) {
|
||||
case '+insert", "undo':
|
||||
start += 1
|
||||
break
|
||||
|
||||
case 'paste':
|
||||
case '+delete':
|
||||
case '+input':
|
||||
if (changeObject.to.ch !== 0 || changeObject.from.ch !== 0) {
|
||||
start += 1
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
this.incrementLines(start, linesAdded, linesRemoved, editor)
|
||||
}
|
||||
|
||||
moveCursorTo (row, col) {}
|
||||
|
||||
scrollToLine (event, num) {
|
||||
@@ -536,6 +851,7 @@ export default class CodeEditor extends React.Component {
|
||||
this.value = this.props.value
|
||||
this.editor.setValue(this.props.value)
|
||||
this.editor.clearHistory()
|
||||
this.restartHighlighting()
|
||||
this.editor.on('change', this.changeHandler)
|
||||
this.editor.refresh()
|
||||
}
|
||||
@@ -548,7 +864,10 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
handleDropImage (dropEvent) {
|
||||
dropEvent.preventDefault()
|
||||
const { storageKey, noteKey } = this.props
|
||||
const {
|
||||
storageKey,
|
||||
noteKey
|
||||
} = this.props
|
||||
attachmentManagement.handleAttachmentDrop(
|
||||
this,
|
||||
storageKey,
|
||||
@@ -561,53 +880,107 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.replaceSelection(imageMd)
|
||||
}
|
||||
|
||||
handlePaste (editor, e) {
|
||||
const clipboardData = e.clipboardData
|
||||
const { storageKey, noteKey } = this.props
|
||||
const dataTransferItem = clipboardData.items[0]
|
||||
const pastedTxt = clipboardData.getData('text')
|
||||
const isURL = str => {
|
||||
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||
return matcher.test(str)
|
||||
}
|
||||
autoDetectLanguage (content) {
|
||||
const res = hljs.highlightAuto(content, Object.keys(languageMaps))
|
||||
this.setMode(languageMaps[res.language])
|
||||
}
|
||||
|
||||
handlePaste (editor, forceSmartPaste) {
|
||||
const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
|
||||
|
||||
const isURL = str => /(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
|
||||
|
||||
const isInLinkTag = editor => {
|
||||
const startCursor = editor.getCursor('start')
|
||||
const prevChar = editor.getRange(
|
||||
{ line: startCursor.line, ch: startCursor.ch - 2 },
|
||||
{ line: startCursor.line, ch: startCursor.ch }
|
||||
)
|
||||
const prevChar = editor.getRange({
|
||||
line: startCursor.line,
|
||||
ch: startCursor.ch - 2
|
||||
}, {
|
||||
line: startCursor.line,
|
||||
ch: startCursor.ch
|
||||
})
|
||||
const endCursor = editor.getCursor('end')
|
||||
const nextChar = editor.getRange(
|
||||
{ line: endCursor.line, ch: endCursor.ch },
|
||||
{ line: endCursor.line, ch: endCursor.ch + 1 }
|
||||
)
|
||||
const nextChar = editor.getRange({
|
||||
line: endCursor.line,
|
||||
ch: endCursor.ch
|
||||
}, {
|
||||
line: endCursor.line,
|
||||
ch: endCursor.ch + 1
|
||||
})
|
||||
return prevChar === '](' && nextChar === ')'
|
||||
}
|
||||
|
||||
const pastedHtml = clipboardData.getData('text/html')
|
||||
if (pastedHtml !== '') {
|
||||
this.handlePasteHtml(e, editor, pastedHtml)
|
||||
} else if (dataTransferItem.type.match('image')) {
|
||||
attachmentManagement.handlePastImageEvent(
|
||||
this,
|
||||
storageKey,
|
||||
noteKey,
|
||||
dataTransferItem
|
||||
)
|
||||
} else if (
|
||||
this.props.fetchUrlTitle &&
|
||||
isURL(pastedTxt) &&
|
||||
!isInLinkTag(editor)
|
||||
) {
|
||||
this.handlePasteUrl(e, editor, pastedTxt)
|
||||
const isInFencedCodeBlock = editor => {
|
||||
const cursor = editor.getCursor()
|
||||
|
||||
let token = editor.getTokenAt(cursor)
|
||||
if (token.state.fencedState) {
|
||||
return true
|
||||
}
|
||||
|
||||
let line = line = cursor.line - 1
|
||||
while (line >= 0) {
|
||||
token = editor.getTokenAt({
|
||||
ch: 3,
|
||||
line
|
||||
})
|
||||
|
||||
if (token.start === token.end) {
|
||||
--line
|
||||
} else if (token.type === 'comment') {
|
||||
if (line > 0) {
|
||||
token = editor.getTokenAt({
|
||||
ch: 3,
|
||||
line: line - 1
|
||||
})
|
||||
|
||||
return token.type !== 'comment'
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||
|
||||
const pastedTxt = clipboard.readText()
|
||||
|
||||
if (isInFencedCodeBlock(editor)) {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(editor, pastedTxt)
|
||||
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||
attachmentManagement
|
||||
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||
.then(modifiedText => {
|
||||
this.editor.replaceSelection(modifiedText)
|
||||
})
|
||||
e.preventDefault()
|
||||
} else {
|
||||
const image = clipboard.readImage()
|
||||
if (!image.isEmpty()) {
|
||||
attachmentManagement.handlePasteNativeImage(
|
||||
this,
|
||||
storageKey,
|
||||
noteKey,
|
||||
image
|
||||
)
|
||||
} else if (enableSmartPaste || forceSmartPaste) {
|
||||
const pastedHtml = clipboard.readHTML()
|
||||
if (pastedHtml.length > 0) {
|
||||
this.handlePasteHtml(editor, pastedHtml)
|
||||
} else {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
}
|
||||
} else {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.props.mode && this.props.autoDetect) {
|
||||
this.autoDetectLanguage(editor.doc.getValue())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,8 +990,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handlePasteUrl (e, editor, pastedTxt) {
|
||||
e.preventDefault()
|
||||
handlePasteUrl (editor, pastedTxt) {
|
||||
const taggedUrl = `<${pastedTxt}>`
|
||||
editor.replaceSelection(taggedUrl)
|
||||
|
||||
@@ -657,12 +1029,15 @@ export default class CodeEditor extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handlePasteHtml (e, editor, pastedHtml) {
|
||||
e.preventDefault()
|
||||
handlePasteHtml (editor, pastedHtml) {
|
||||
const markdown = this.turndownService.turndown(pastedHtml)
|
||||
editor.replaceSelection(markdown)
|
||||
}
|
||||
|
||||
handlePasteText (editor, pastedTxt) {
|
||||
editor.replaceSelection(pastedTxt)
|
||||
}
|
||||
|
||||
mapNormalResponse (response, pastedTxt) {
|
||||
return this.decodeResponse(response).then(body => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -683,6 +1058,29 @@ export default class CodeEditor extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
initialHighlighting () {
|
||||
if (this.editor.options.linesHighlighted == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const totalHighlightedLines = this.editor.options.linesHighlighted.length
|
||||
const totalAvailableLines = this.editor.lineCount()
|
||||
|
||||
for (let i = 0; i < totalHighlightedLines; i++) {
|
||||
const lineNumber = this.editor.options.linesHighlighted[i]
|
||||
if (lineNumber > totalAvailableLines) {
|
||||
// make sure that we skip the invalid lines althrough this case should not be happened.
|
||||
continue
|
||||
}
|
||||
this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background')
|
||||
}
|
||||
}
|
||||
|
||||
restartHighlighting () {
|
||||
this.editor.options.linesHighlighted = this.props.linesHighlighted
|
||||
this.initialHighlighting()
|
||||
}
|
||||
|
||||
mapImageResponse (response, pastedTxt) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
@@ -708,7 +1106,7 @@ export default class CodeEditor extends React.Component {
|
||||
iconv.encodingExists(_charset)
|
||||
? _charset
|
||||
: 'utf-8'
|
||||
resolve(iconv.decode(new Buffer(buff), charset).toString())
|
||||
resolve(iconv.decode(Buffer.from(buff), charset).toString())
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
@@ -728,20 +1126,28 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {className, fontSize} = this.props
|
||||
const {
|
||||
className,
|
||||
fontSize
|
||||
} = this.props
|
||||
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
|
||||
const width = this.props.width
|
||||
return (
|
||||
<div
|
||||
className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize: fontSize,
|
||||
width: width
|
||||
}}
|
||||
onDrop={e => this.handleDropImage(e)}
|
||||
return (<
|
||||
div className={
|
||||
className == null ? 'CodeEditor' : `CodeEditor ${className}`
|
||||
}
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={
|
||||
{
|
||||
fontFamily,
|
||||
fontSize: fontSize,
|
||||
width: width
|
||||
}
|
||||
}
|
||||
onDrop={
|
||||
e => this.handleDropImage(e)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -775,6 +1181,7 @@ CodeEditor.propTypes = {
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
autoDetect: PropTypes.bool,
|
||||
spellCheck: PropTypes.bool
|
||||
}
|
||||
|
||||
@@ -786,5 +1193,6 @@ CodeEditor.defaultProps = {
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space',
|
||||
autoDetect: false,
|
||||
spellCheck: false
|
||||
}
|
||||
|
||||
68
browser/components/ColorPicker.js
Normal file
68
browser/components/ColorPicker.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { SketchPicker } from 'react-color'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ColorPicker.styl'
|
||||
|
||||
const componentHeight = 330
|
||||
|
||||
class ColorPicker extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
color: this.props.color || '#939395'
|
||||
}
|
||||
|
||||
this.onColorChange = this.onColorChange.bind(this)
|
||||
this.handleConfirm = this.handleConfirm.bind(this)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.onColorChange(nextProps.color)
|
||||
}
|
||||
|
||||
onColorChange (color) {
|
||||
this.setState({
|
||||
color
|
||||
})
|
||||
}
|
||||
|
||||
handleConfirm () {
|
||||
this.props.onConfirm(this.state.color)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { onReset, onCancel, targetRect } = this.props
|
||||
const { color } = this.state
|
||||
|
||||
const clientHeight = document.body.clientHeight
|
||||
const alignX = targetRect.right + 4
|
||||
let alignY = targetRect.top
|
||||
if (targetRect.top + componentHeight > clientHeight) {
|
||||
alignY = targetRect.bottom - componentHeight
|
||||
}
|
||||
|
||||
return (
|
||||
<div styleName='colorPicker' style={{top: `${alignY}px`, left: `${alignX}px`}}>
|
||||
<div styleName='cover' onClick={onCancel} />
|
||||
<SketchPicker color={color} onChange={this.onColorChange} />
|
||||
<div styleName='footer'>
|
||||
<button styleName='btn-reset' onClick={onReset}>Reset</button>
|
||||
<button styleName='btn-cancel' onClick={onCancel}>Cancel</button>
|
||||
<button styleName='btn-confirm' onClick={this.handleConfirm}>Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ColorPicker.propTypes = {
|
||||
color: PropTypes.string,
|
||||
targetRect: PropTypes.object,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(ColorPicker, styles)
|
||||
39
browser/components/ColorPicker.styl
Normal file
39
browser/components/ColorPicker.styl
Normal file
@@ -0,0 +1,39 @@
|
||||
.colorPicker
|
||||
position fixed
|
||||
z-index 2
|
||||
display flex
|
||||
flex-direction column
|
||||
|
||||
.cover
|
||||
position fixed
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
left 0
|
||||
|
||||
.footer
|
||||
display flex
|
||||
justify-content center
|
||||
z-index 2
|
||||
align-items center
|
||||
& > button + button
|
||||
margin-left 10px
|
||||
|
||||
.btn-cancel,
|
||||
.btn-confirm,
|
||||
.btn-reset
|
||||
vertical-align middle
|
||||
height 25px
|
||||
margin-top 2.5px
|
||||
border-radius 2px
|
||||
border none
|
||||
padding 0 5px
|
||||
background-color $default-button-background
|
||||
&:hover
|
||||
background-color $default-button-background--hover
|
||||
.btn-confirm
|
||||
background-color #1EC38B
|
||||
&:hover
|
||||
background-color darken(#1EC38B, 25%)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||
|
||||
class MarkdownEditor extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -19,10 +20,10 @@ class MarkdownEditor extends React.Component {
|
||||
this.supportMdSelectionBold = [16, 17, 186]
|
||||
|
||||
this.state = {
|
||||
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
|
||||
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE',
|
||||
renderValue: props.value,
|
||||
keyPressed: new Set(),
|
||||
isLocked: false
|
||||
isLocked: props.isLocked
|
||||
}
|
||||
|
||||
this.lockEditorCode = () => this.handleLockEditor()
|
||||
@@ -75,6 +76,7 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
if (this.state.isLocked) return
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
|
||||
@@ -221,6 +223,28 @@ class MarkdownEditor extends React.Component {
|
||||
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
|
||||
}
|
||||
|
||||
handleDropImage (dropEvent) {
|
||||
dropEvent.preventDefault()
|
||||
const { storageKey, noteKey } = this.props
|
||||
|
||||
this.setState({
|
||||
status: 'CODE'
|
||||
}, () => {
|
||||
this.refs.code.focus()
|
||||
|
||||
this.refs.code.editor.execCommand('goDocEnd')
|
||||
this.refs.code.editor.execCommand('goLineEnd')
|
||||
this.refs.code.editor.execCommand('newlineAndIndent')
|
||||
|
||||
attachmentManagement.handleAttachmentDrop(
|
||||
this.refs.code,
|
||||
storageKey,
|
||||
noteKey,
|
||||
dropEvent
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyUp (e) {
|
||||
const keyPressed = this.state.keyPressed
|
||||
keyPressed.delete(e.keyCode)
|
||||
@@ -232,7 +256,7 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {className, value, config, storageKey, noteKey} = this.props
|
||||
const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
@@ -270,14 +294,21 @@ class MarkdownEditor extends React.Component {
|
||||
enableRulers={config.editor.enableRulers}
|
||||
rulers={config.editor.rulers}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
linesHighlighted={linesHighlighted}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
spellCheck={config.editor.spellcheck}
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
switchPreview={config.editor.switchPreview}
|
||||
/>
|
||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||
? 'preview'
|
||||
@@ -311,6 +342,7 @@ class MarkdownEditor extends React.Component {
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||
onDrop={(e) => this.handleDropImage(e)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -21,6 +21,8 @@ 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 Carousel from 'react-image-carousel'
|
||||
import ConfigManager from '../main/lib/ConfigManager'
|
||||
|
||||
const { remote, shell } = require('electron')
|
||||
@@ -40,7 +42,8 @@ const appPath = fileUrl(
|
||||
)
|
||||
const CSS_FILES = [
|
||||
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`,
|
||||
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
|
||||
]
|
||||
|
||||
function buildStyle (
|
||||
@@ -207,7 +210,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||
this.printHandler = () => this.handlePrint()
|
||||
|
||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
||||
this.linkClickHandler = this.handleLinkClick.bind(this)
|
||||
this.initMarkdown = this.initMarkdown.bind(this)
|
||||
this.initMarkdown()
|
||||
}
|
||||
@@ -291,26 +294,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleSaveAsMd () {
|
||||
this.exportAsDocument('md', (noteContent, exportTasks) => {
|
||||
let result = noteContent
|
||||
if (this.props && this.props.storagePath && this.props.noteKey) {
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
noteContent,
|
||||
this.props.storagePath
|
||||
)
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
result = attachmentManagement.removeStorageAndNoteReferences(
|
||||
noteContent,
|
||||
this.props.noteKey
|
||||
)
|
||||
}
|
||||
return result
|
||||
})
|
||||
this.exportAsDocument('md')
|
||||
}
|
||||
|
||||
handleSaveAsHtml () {
|
||||
@@ -339,11 +323,6 @@ export default class MarkdownPreview extends React.Component {
|
||||
)
|
||||
let body = this.markdown.render(noteContent)
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
noteContent,
|
||||
this.props.storagePath
|
||||
)
|
||||
|
||||
files.forEach(file => {
|
||||
if (global.process.platform === 'win32') {
|
||||
file = file.replace('file:///', '')
|
||||
@@ -355,16 +334,6 @@ export default class MarkdownPreview extends React.Component {
|
||||
dst: 'css'
|
||||
})
|
||||
})
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
body = attachmentManagement.removeStorageAndNoteReferences(
|
||||
body,
|
||||
this.props.noteKey
|
||||
)
|
||||
|
||||
let styles = ''
|
||||
files.forEach(file => {
|
||||
@@ -397,8 +366,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
if (filename) {
|
||||
const content = this.props.value
|
||||
const storage = this.props.storagePath
|
||||
const nodeKey = this.props.noteKey
|
||||
|
||||
exportNote(storage, content, filename, contentFormatter)
|
||||
exportNote(nodeKey, storage, content, filename, contentFormatter)
|
||||
.then(res => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
@@ -428,6 +398,31 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Convert special characters between three ```
|
||||
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
|
||||
* @returns {string} HTML in which special characters between three ``` have been converted
|
||||
*/
|
||||
escapeHtmlCharactersInCodeTag (splitWithCodeTag) {
|
||||
for (let index = 0; index < splitWithCodeTag.length; index++) {
|
||||
const codeTagRequired = (splitWithCodeTag[index] !== '\`\`\`' && index < splitWithCodeTag.length - 1)
|
||||
if (codeTagRequired) {
|
||||
splitWithCodeTag.splice((index + 1), 0, '\`\`\`')
|
||||
}
|
||||
}
|
||||
let inCodeTag = false
|
||||
let result = ''
|
||||
for (let content of splitWithCodeTag) {
|
||||
if (content === '\`\`\`') {
|
||||
inCodeTag = !inCodeTag
|
||||
} else if (inCodeTag) {
|
||||
content = escapeHtmlCharacters(content)
|
||||
}
|
||||
result += content
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
getScrollBarStyle () {
|
||||
const { theme } = this.props
|
||||
|
||||
@@ -443,6 +438,8 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { onDrop } = this.props
|
||||
|
||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||
this.refs.root.contentWindow.document.body.addEventListener(
|
||||
'contextmenu',
|
||||
@@ -480,7 +477,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'drop',
|
||||
this.preventImageDroppedHandler
|
||||
onDrop || this.preventImageDroppedHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'dragover',
|
||||
@@ -497,6 +494,8 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onDrop } = this.props
|
||||
|
||||
this.refs.root.contentWindow.document.body.removeEventListener(
|
||||
'contextmenu',
|
||||
this.contextMenuHandler
|
||||
@@ -515,7 +514,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'drop',
|
||||
this.preventImageDroppedHandler
|
||||
onDrop || this.preventImageDroppedHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'dragover',
|
||||
@@ -658,11 +657,16 @@ export default class MarkdownPreview extends React.Component {
|
||||
indentSize,
|
||||
showCopyNotification,
|
||||
storagePath,
|
||||
noteKey
|
||||
noteKey,
|
||||
sanitize
|
||||
} = this.props
|
||||
let { value, codeBlockTheme } = this.props
|
||||
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
if (sanitize === 'NONE') {
|
||||
const splitWithCodeTag = value.split('```')
|
||||
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag)
|
||||
}
|
||||
const renderedHTML = this.markdown.render(value)
|
||||
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
|
||||
@@ -800,6 +804,109 @@ export default class MarkdownPreview extends React.Component {
|
||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
|
||||
}
|
||||
)
|
||||
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.gallery'),
|
||||
el => {
|
||||
const images = el.innerHTML.split(/\n/g).filter(i => i.length > 0)
|
||||
el.innerHTML = ''
|
||||
|
||||
const height = el.attributes.getNamedItem('data-height')
|
||||
if (height && height.value !== 'undefined') {
|
||||
el.style.height = height.value + 'vh'
|
||||
}
|
||||
|
||||
let autoplay = el.attributes.getNamedItem('data-autoplay')
|
||||
if (autoplay && autoplay.value !== 'undefined') {
|
||||
autoplay = parseInt(autoplay.value, 10) || 0
|
||||
} else {
|
||||
autoplay = 0
|
||||
}
|
||||
|
||||
render(
|
||||
<Carousel
|
||||
images={images}
|
||||
autoplay={autoplay}
|
||||
/>,
|
||||
el
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
|
||||
const rect = markdownPreviewIframe.getBoundingClientRect()
|
||||
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
|
||||
for (const img of imgList) {
|
||||
img.onclick = () => {
|
||||
const widthMagnification = document.body.clientWidth / img.width
|
||||
const heightMagnification = document.body.clientHeight / img.height
|
||||
const baseOnWidth = widthMagnification < heightMagnification
|
||||
const magnification = baseOnWidth ? widthMagnification : heightMagnification
|
||||
|
||||
const zoomImgWidth = img.width * magnification
|
||||
const zoomImgHeight = img.height * magnification
|
||||
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
|
||||
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
|
||||
const originalImgTop = img.y + rect.top
|
||||
const originalImgLeft = img.x + rect.left
|
||||
const originalImgRect = {
|
||||
top: `${originalImgTop}px`,
|
||||
left: `${originalImgLeft}px`,
|
||||
width: `${img.width}px`,
|
||||
height: `${img.height}px`
|
||||
}
|
||||
const zoomInImgRect = {
|
||||
top: `${baseOnWidth ? zoomImgTop : 0}px`,
|
||||
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
|
||||
width: `${zoomImgWidth}px`,
|
||||
height: `${zoomImgHeight}px`
|
||||
}
|
||||
const animationSpeed = 300
|
||||
|
||||
const zoomImg = document.createElement('img')
|
||||
zoomImg.src = img.src
|
||||
zoomImg.style = `
|
||||
position: absolute;
|
||||
top: ${baseOnWidth ? zoomImgTop : 0}px;
|
||||
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
|
||||
width: ${zoomImgWidth};
|
||||
height: ${zoomImgHeight}px;
|
||||
`
|
||||
zoomImg.animate([
|
||||
originalImgRect,
|
||||
zoomInImgRect
|
||||
], animationSpeed)
|
||||
|
||||
const overlay = document.createElement('div')
|
||||
overlay.style = `
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
cursor: zoom-out;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: ${document.body.clientHeight}px;
|
||||
z-index: 100;
|
||||
`
|
||||
overlay.onclick = () => {
|
||||
zoomImg.style = `
|
||||
position: absolute;
|
||||
top: ${originalImgTop}px;
|
||||
left: ${originalImgLeft}px;
|
||||
width: ${img.width}px;
|
||||
height: ${img.height}px;
|
||||
`
|
||||
const zoomOutImgAnimation = zoomImg.animate([
|
||||
zoomInImgRect,
|
||||
originalImgRect
|
||||
], animationSpeed)
|
||||
zoomOutImgAnimation.onfinish = () => overlay.remove()
|
||||
}
|
||||
|
||||
overlay.appendChild(zoomImg)
|
||||
document.body.appendChild(overlay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -842,7 +949,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
handlelinkClick (e) {
|
||||
handleLinkClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ class MarkdownSplitEditor extends React.Component {
|
||||
this.refs.code.setValue(value)
|
||||
}
|
||||
|
||||
handleOnChange () {
|
||||
handleOnChange (e) {
|
||||
this.value = this.refs.code.value
|
||||
this.props.onChange()
|
||||
this.props.onChange(e)
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
@@ -136,7 +136,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {config, value, storageKey, noteKey} = this.props
|
||||
const {config, value, storageKey, noteKey, linesHighlighted} = this.props
|
||||
const storage = findStorage(storageKey)
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
@@ -160,6 +160,9 @@ class MarkdownSplitEditor extends React.Component {
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
enableRulers={config.editor.enableRulers}
|
||||
@@ -169,9 +172,13 @@ class MarkdownSplitEditor extends React.Component {
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
linesHighlighted={linesHighlighted}
|
||||
onChange={(e) => this.handleOnChange(e)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
spellCheck={config.editor.spellcheck}
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
switchPreview={config.editor.switchPreview}
|
||||
/>
|
||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||
<div styleName='slider-hitbox' />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { isArray } from 'lodash'
|
||||
import invertColor from 'invert-color'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
||||
import styles from './NoteItem.styl'
|
||||
@@ -13,29 +14,38 @@ import i18n from 'browser/lib/i18n'
|
||||
/**
|
||||
* @description Tag element component.
|
||||
* @param {string} tagName
|
||||
* @param {string} color
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const TagElement = ({ tagName }) => (
|
||||
<span styleName='item-bottom-tagList-item' key={tagName}>
|
||||
#{tagName}
|
||||
</span>
|
||||
)
|
||||
const TagElement = ({ tagName, color }) => {
|
||||
const style = {}
|
||||
if (color) {
|
||||
style.backgroundColor = color
|
||||
style.color = invertColor(color, { black: '#222', white: '#f1f1f1', threshold: 0.3 })
|
||||
}
|
||||
return (
|
||||
<span styleName='item-bottom-tagList-item' key={tagName} style={style}>
|
||||
#{tagName}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Tag element list component.
|
||||
* @param {Array|null} tags
|
||||
* @param {boolean} showTagsAlphabetically
|
||||
* @param {Object} coloredTags
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const TagElementList = (tags, showTagsAlphabetically) => {
|
||||
const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
|
||||
if (!isArray(tags)) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (showTagsAlphabetically) {
|
||||
return _.sortBy(tags).map(tag => TagElement({ tagName: tag }))
|
||||
return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||
} else {
|
||||
return tags.map(tag => TagElement({ tagName: tag }))
|
||||
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +56,7 @@ const TagElementList = (tags, showTagsAlphabetically) => {
|
||||
* @param {Function} handleNoteClick
|
||||
* @param {Function} handleNoteContextMenu
|
||||
* @param {Function} handleDragStart
|
||||
* @param {Object} coloredTags
|
||||
* @param {string} dateDisplay
|
||||
*/
|
||||
const NoteItem = ({
|
||||
@@ -59,7 +70,8 @@ const NoteItem = ({
|
||||
storageName,
|
||||
folderName,
|
||||
viewType,
|
||||
showTagsAlphabetically
|
||||
showTagsAlphabetically,
|
||||
coloredTags
|
||||
}) => (
|
||||
<div
|
||||
styleName={isActive ? 'item--active' : 'item'}
|
||||
@@ -97,7 +109,7 @@ const NoteItem = ({
|
||||
<div styleName='item-bottom'>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{note.tags.length > 0
|
||||
? TagElementList(note.tags, showTagsAlphabetically)
|
||||
? TagElementList(note.tags, showTagsAlphabetically, coloredTags)
|
||||
: <span
|
||||
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
||||
styleName='item-bottom-tagList-empty'
|
||||
@@ -127,6 +139,7 @@ const NoteItem = ({
|
||||
NoteItem.propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
dateDisplay: PropTypes.string.isRequired,
|
||||
coloredTags: PropTypes.object,
|
||||
note: PropTypes.shape({
|
||||
storage: PropTypes.string.isRequired,
|
||||
key: PropTypes.string.isRequired,
|
||||
|
||||
@@ -3,19 +3,30 @@
|
||||
flex 1
|
||||
min-width 70px
|
||||
overflow hidden
|
||||
border-left 1px solid $ui-borderColor
|
||||
border-top 1px solid $ui-borderColor
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||
.deleteButton
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-backgroundColor, 15%)
|
||||
&:active
|
||||
color white
|
||||
background-color $ui-active-color
|
||||
color: $ui-text-color
|
||||
visibility visible
|
||||
transition 0.15s
|
||||
.button
|
||||
color: $ui-text-color
|
||||
transition 0.15s
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
min-width 100px
|
||||
border-bottom $ui-border
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
.deleteButton
|
||||
visibility visible
|
||||
color: $ui-text-color
|
||||
transition 0.15s
|
||||
.button
|
||||
font-weight bold
|
||||
color: $ui-text-color
|
||||
transition 0.15s
|
||||
|
||||
.button
|
||||
width 100%
|
||||
@@ -27,8 +38,7 @@
|
||||
background-color transparent
|
||||
transition 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.deleteButton
|
||||
position absolute
|
||||
@@ -42,6 +52,7 @@
|
||||
color $ui-inactive-text-color
|
||||
background-color transparent
|
||||
border-radius 2px
|
||||
visibility hidden
|
||||
|
||||
.input
|
||||
height 29px
|
||||
@@ -50,76 +61,66 @@
|
||||
width 100%
|
||||
outline none
|
||||
|
||||
body[data-theme="default"], body[data-theme="white"]
|
||||
.root--active
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
border-top 1px solid $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
transition 0.15s
|
||||
.button
|
||||
color $ui-dark-text-color
|
||||
transition 0.15s
|
||||
.deleteButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
transition 0.15s
|
||||
|
||||
.root--active
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
border-top 1px solid $ui-dark-borderColor
|
||||
.button
|
||||
color $ui-dark-text-color
|
||||
.deleteButton
|
||||
color $ui-dark-text-color
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-dark-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-dark-text-color, 30%)
|
||||
transition 0.15s
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-solarized-dark-text-color
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
transition 0.15s
|
||||
.deleteButton
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-solarized-dark-button--active-color
|
||||
transition 0.15s
|
||||
.button
|
||||
color $ui-solarized-dark-button--active-color
|
||||
transition 0.15s
|
||||
|
||||
.root--active
|
||||
color $ui-solarized-dark-button--active-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
.deleteButton
|
||||
color $ui-solarized-dark-button--active-color
|
||||
.button
|
||||
color $ui-solarized-dark-button--active-color
|
||||
|
||||
.button
|
||||
border none
|
||||
@@ -127,101 +128,75 @@ body[data-theme="solarized-dark"]
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-solarized-dark-text-color, 30%)
|
||||
color $ui-solarized-dark-button--active-color
|
||||
transition 0.15s
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-monokai-borderColor
|
||||
&:hover
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
transition 0.15s
|
||||
.deleteButton
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
transition 0.15s
|
||||
.button
|
||||
color $ui-monokai-text-color
|
||||
transition 0.15s
|
||||
|
||||
.root--active
|
||||
color $ui-monokai-active-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
border-color $ui-monokai-borderColor
|
||||
.deleteButton
|
||||
color $ui-monokai-text-color
|
||||
.button
|
||||
color $ui-monokai-active-color
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-monokai-text-color
|
||||
color $ui-inactive-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-monokai-text-color, 30%)
|
||||
transition 0.15s
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dracula-borderColor
|
||||
&:hover
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
transition 0.15s
|
||||
.deleteButton
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
transition 0.15s
|
||||
.button
|
||||
color $ui-dracula-text-color
|
||||
transition 0.15s
|
||||
|
||||
.root--active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
border-color $ui-dracula-borderColor
|
||||
.deleteButton
|
||||
color $ui-dracula-text-color
|
||||
.button
|
||||
color $ui-dracula-active-color
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-dracula-text-color
|
||||
color $ui-inactive-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-dracula-text-color, 30%)
|
||||
color $ui-dracula-text-color
|
||||
@@ -10,11 +10,12 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
* @param {string} name
|
||||
* @param {Function} handleClickTagListItem
|
||||
* @param {Function} handleClickNarrowToTag
|
||||
* @param {bool} isActive
|
||||
* @param {bool} isRelated
|
||||
* @param {boolean} isActive
|
||||
* @param {boolean} isRelated
|
||||
* @param {string} bgColor tab backgroundColor
|
||||
*/
|
||||
|
||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => (
|
||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count, color}) => (
|
||||
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}>
|
||||
{isRelated
|
||||
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
||||
@@ -23,6 +24,7 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, hand
|
||||
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
||||
}
|
||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||
<span styleName='tagList-item-color' style={{backgroundColor: color || 'transparent'}} />
|
||||
<span styleName='tagList-item-name'>
|
||||
{`# ${name}`}
|
||||
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
|
||||
@@ -33,7 +35,8 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, hand
|
||||
|
||||
TagListItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
handleClickTagListItem: PropTypes.func.isRequired
|
||||
handleClickTagListItem: PropTypes.func.isRequired,
|
||||
color: PropTypes.string
|
||||
}
|
||||
|
||||
export default CSSModules(TagListItem, styles)
|
||||
|
||||
@@ -71,6 +71,11 @@
|
||||
padding-right 15px
|
||||
font-size 13px
|
||||
|
||||
.tagList-item-color
|
||||
height 26px
|
||||
width 3px
|
||||
display inline-block
|
||||
|
||||
body[data-theme="white"]
|
||||
.tagList-item
|
||||
color $ui-inactive-text-color
|
||||
|
||||
@@ -55,11 +55,14 @@ body
|
||||
line-height 1.6
|
||||
overflow-x hidden
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
// do not allow display line breaks
|
||||
.katex-display > .katex
|
||||
white-space nowrap
|
||||
// allow inline line breaks
|
||||
.katex
|
||||
font 400 1.2em 'KaTeX_Main'
|
||||
line-height 1.2em
|
||||
white-space initial
|
||||
text-indent 0
|
||||
.katex .katex-html
|
||||
display inline-flex
|
||||
.katex .mfrac>.vlist>span:nth-child(2)
|
||||
top 0 !important
|
||||
.katex-error
|
||||
@@ -162,6 +165,7 @@ p
|
||||
white-space pre-line
|
||||
word-wrap break-word
|
||||
img
|
||||
cursor zoom-in
|
||||
max-width 100%
|
||||
strong, b
|
||||
font-weight bold
|
||||
@@ -183,6 +187,10 @@ ul
|
||||
display list-item
|
||||
&.taskListItem
|
||||
list-style none
|
||||
&>input
|
||||
margin-left -1.6em
|
||||
&>p
|
||||
margin-left -1.8em
|
||||
p
|
||||
margin 0
|
||||
&>li>ul, &>li>ol
|
||||
@@ -416,6 +424,26 @@ pre.fence
|
||||
canvas, svg
|
||||
max-width 100% !important
|
||||
|
||||
.gallery
|
||||
width 100%
|
||||
height 50vh
|
||||
|
||||
.carousel
|
||||
.carousel-main img
|
||||
min-width auto
|
||||
max-width 100%
|
||||
min-height auto
|
||||
max-height 100%
|
||||
|
||||
.carousel-footer::-webkit-scrollbar-corner
|
||||
background-color transparent
|
||||
|
||||
.carousel-main, .carousel-footer
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
.prev, .next
|
||||
color $ui-text-color
|
||||
background-color $ui-tag-backgroundColor
|
||||
|
||||
themeDarkBackground = darken(#21252B, 10%)
|
||||
themeDarkText = #f9f9f9
|
||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||
@@ -475,6 +503,14 @@ body[data-theme="dark"]
|
||||
border-color themeDarkBorder
|
||||
background-color themeDarkPreview
|
||||
|
||||
pre.fence
|
||||
.gallery
|
||||
.carousel-main, .carousel-footer
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
.prev, .next
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-tag-backgroundColor
|
||||
|
||||
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
||||
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
|
||||
@@ -510,6 +546,14 @@ body[data-theme="solarized-dark"]
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
pre.fence
|
||||
.gallery
|
||||
.carousel-main, .carousel-footer
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
.prev, .next
|
||||
color $ui-solarized-dark-button--active-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
|
||||
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
||||
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
||||
themeMonokaiTableHead = themeMonokaiTableEven
|
||||
@@ -538,6 +582,7 @@ body[data-theme="monokai"]
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
kbd
|
||||
background-color themeDarkBackground
|
||||
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeMonokaiTableHead
|
||||
@@ -547,6 +592,14 @@ body[data-theme="monokai"]
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
pre.fence
|
||||
.gallery
|
||||
.carousel-main, .carousel-footer
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
.prev, .next
|
||||
color $ui-monokai-button--active-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
|
||||
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
|
||||
themeDraculaTableHead = themeDraculaTableEven
|
||||
@@ -575,6 +628,7 @@ body[data-theme="dracula"]
|
||||
border-right solid 1px themeDraculaTableBorder
|
||||
kbd
|
||||
background-color themeDarkBackground
|
||||
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeDraculaTableHead
|
||||
@@ -583,3 +637,11 @@ body[data-theme="dracula"]
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
pre.fence
|
||||
.gallery
|
||||
.carousel-main, .carousel-footer
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
.prev, .next
|
||||
color $ui-dracula-button--active-color
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
|
||||
@@ -2,51 +2,27 @@
|
||||
* @fileoverview Markdown table of contents generator
|
||||
*/
|
||||
|
||||
import { EOL } from 'os'
|
||||
import toc from 'markdown-toc'
|
||||
import diacritics from 'diacritics-map'
|
||||
import stripColor from 'strip-color'
|
||||
import mdlink from 'markdown-link'
|
||||
import slugify from './slugify'
|
||||
|
||||
const EOL = require('os').EOL
|
||||
const hasProp = Object.prototype.hasOwnProperty
|
||||
|
||||
/**
|
||||
* @caseSensitiveSlugify Custom slugify function
|
||||
* Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js),
|
||||
* but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067
|
||||
* From @enyaxu/markdown-it-anchor
|
||||
*/
|
||||
function caseSensitiveSlugify (str) {
|
||||
function replaceDiacritics (str) {
|
||||
return str.replace(/[À-ž]/g, function (ch) {
|
||||
return diacritics[ch] || ch
|
||||
})
|
||||
}
|
||||
|
||||
function getTitle (str) {
|
||||
if (/^\[[^\]]+\]\(/.test(str)) {
|
||||
var m = /^\[([^\]]+)\]/.exec(str)
|
||||
if (m) return m[1]
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
str = getTitle(str)
|
||||
str = stripColor(str)
|
||||
// str = str.toLowerCase() //let's be case sensitive
|
||||
|
||||
// `.split()` is often (but not always) faster than `.replace()`
|
||||
str = str.split(' ').join('-')
|
||||
str = str.split(/\t/).join('--')
|
||||
str = str.split(/<\/?[^>]+>/).join('')
|
||||
str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('')
|
||||
str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('')
|
||||
str = replaceDiacritics(str)
|
||||
return str
|
||||
function uniqueSlug (slug, slugs, opts) {
|
||||
let uniq = slug
|
||||
let i = opts.uniqueSlugStartIndex
|
||||
while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
|
||||
slugs[uniq] = true
|
||||
return uniq
|
||||
}
|
||||
|
||||
function linkify (tok, text, slug, opts) {
|
||||
var uniqeID = opts.num === 0 ? '' : '-' + opts.num
|
||||
tok.content = mdlink(text, '#' + slug + uniqeID)
|
||||
return tok
|
||||
function linkify (token) {
|
||||
token.content = mdlink(token.content, '#' + token.slug)
|
||||
return token
|
||||
}
|
||||
|
||||
const TOC_MARKER_START = '<!-- toc -->'
|
||||
@@ -91,8 +67,23 @@ export function generateInEditor (editor) {
|
||||
* @returns generatedTOC String containing generated TOC
|
||||
*/
|
||||
export function generate (markdownText) {
|
||||
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify, linkify: linkify})
|
||||
return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END
|
||||
const slugs = {}
|
||||
const opts = {
|
||||
uniqueSlugStartIndex: 1
|
||||
}
|
||||
|
||||
const result = toc(markdownText, {
|
||||
slugify: title => {
|
||||
return uniqueSlug(slugify(title), slugs, opts)
|
||||
},
|
||||
linkify: false
|
||||
})
|
||||
|
||||
const md = toc.bullets(result.json.map(linkify), {
|
||||
highest: result.highest
|
||||
})
|
||||
|
||||
return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
|
||||
}
|
||||
|
||||
function wrapTocWithEol (toc, editor) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import katex from 'katex'
|
||||
import { lastFindInArray } from './utils'
|
||||
import anchor from '@enyaxu/markdown-it-anchor'
|
||||
|
||||
function createGutter (str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
@@ -118,14 +117,8 @@ class Markdown {
|
||||
this.md.use(require('markdown-it-imsize'))
|
||||
this.md.use(require('markdown-it-footnote'))
|
||||
this.md.use(require('markdown-it-multimd-table'))
|
||||
this.md.use(anchor, {
|
||||
slugify: (title) => {
|
||||
var slug = encodeURI(title.trim()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||
.replace(/\s+/g, '-'))
|
||||
.replace(/\-+$/, '')
|
||||
return slug
|
||||
}
|
||||
this.md.use(require('@enyaxu/markdown-it-anchor'), {
|
||||
slugify: require('./slugify')
|
||||
})
|
||||
this.md.use(require('markdown-it-kbd'))
|
||||
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
|
||||
@@ -152,6 +145,21 @@ class Markdown {
|
||||
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
|
||||
</pre>`
|
||||
},
|
||||
gallery: token => {
|
||||
const content = token.content.split('\n').slice(0, -1).map(line => {
|
||||
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
|
||||
if (match) {
|
||||
return match[1]
|
||||
} else {
|
||||
return line
|
||||
}
|
||||
}).join('\n')
|
||||
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="gallery" data-autoplay="${token.parameters.autoplay}" data-height="${token.parameters.height}">${content}</div>
|
||||
</pre>`
|
||||
},
|
||||
mermaid: token => {
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
|
||||
@@ -18,7 +18,8 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
|
||||
folder: folder,
|
||||
title: '',
|
||||
tags,
|
||||
content: ''
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
})
|
||||
.then(note => {
|
||||
const noteHash = note.key
|
||||
@@ -45,6 +46,8 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
|
||||
tags = params.tagname.split(' ')
|
||||
}
|
||||
|
||||
const defaultLanguage = config.editor.snippetDefaultLanguage === 'Auto Detect' ? null : config.editor.snippetDefaultLanguage
|
||||
|
||||
return dataApi
|
||||
.createNote(storage, {
|
||||
type: 'SNIPPET_NOTE',
|
||||
@@ -55,8 +58,9 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
|
||||
snippets: [
|
||||
{
|
||||
name: '',
|
||||
mode: config.editor.snippetDefaultLanguage || 'text',
|
||||
content: ''
|
||||
mode: defaultLanguage,
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
17
browser/lib/slugify.js
Normal file
17
browser/lib/slugify.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import diacritics from 'diacritics-map'
|
||||
|
||||
function replaceDiacritics (str) {
|
||||
return str.replace(/[À-ž]/g, function (ch) {
|
||||
return diacritics[ch] || ch
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function slugify (title) {
|
||||
let slug = title.trim()
|
||||
|
||||
slug = replaceDiacritics(slug)
|
||||
|
||||
slug = slug.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
|
||||
|
||||
return encodeURI(slug).replace(/\-+$/, '')
|
||||
}
|
||||
@@ -5,15 +5,17 @@ import styles from './FullscreenButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
||||
const FullscreenButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
||||
</button>
|
||||
)
|
||||
}) => {
|
||||
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
||||
return (
|
||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
FullscreenButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
right 35px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
@@ -70,22 +70,22 @@ class InfoPanel extends React.Component {
|
||||
<hr />
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
||||
<i className='fa fa-file-code-o' />
|
||||
<p>{i18n.__('.md')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
||||
<i className='fa fa-file-text-o' />
|
||||
<p>{i18n.__('.txt')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||
<i className='fa fa-html5' />
|
||||
<p>{i18n.__('.html')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => print(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
|
||||
<i className='fa fa-print' />
|
||||
<p>{i18n.__('Print')}</p>
|
||||
</button>
|
||||
|
||||
@@ -31,17 +31,17 @@ const InfoPanelTrashed = ({
|
||||
</div>
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
||||
<i className='fa fa-file-code-o' />
|
||||
<p>.md</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
||||
<i className='fa fa-file-text-o' />
|
||||
<p>.txt</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||
<i className='fa fa-html5' />
|
||||
<p>.html</p>
|
||||
</button>
|
||||
|
||||
@@ -39,12 +39,15 @@ class MarkdownNoteDetail extends React.Component {
|
||||
isMovingNote: false,
|
||||
note: Object.assign({
|
||||
title: '',
|
||||
content: ''
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}, props.note),
|
||||
isLockButtonShown: false,
|
||||
isLockButtonShown: props.config.editor.type !== 'SPLIT',
|
||||
isLocked: false,
|
||||
editorType: props.config.editor.type
|
||||
editorType: props.config.editor.type,
|
||||
switchPreview: props.config.editor.switchPreview
|
||||
}
|
||||
|
||||
this.dispatchTimer = null
|
||||
|
||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||
@@ -63,6 +66,9 @@ class MarkdownNoteDetail extends React.Component {
|
||||
})
|
||||
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
|
||||
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) {
|
||||
@@ -71,7 +77,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
this.setState({
|
||||
note: Object.assign({}, nextProps.note)
|
||||
note: Object.assign({linesHighlighted: []}, nextProps.note)
|
||||
}, () => {
|
||||
this.refs.content.reload()
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
@@ -292,7 +298,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
handleToggleLockButton (event, noteStatus) {
|
||||
// first argument event is not used
|
||||
if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
|
||||
if (noteStatus === 'CODE') {
|
||||
this.setState({isLockButtonShown: true})
|
||||
} else {
|
||||
this.setState({isLockButtonShown: false})
|
||||
@@ -318,7 +324,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
handleSwitchMode (type) {
|
||||
this.setState({ editorType: type }, () => {
|
||||
// If in split mode, hide the lock button
|
||||
this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => {
|
||||
this.focus()
|
||||
const newConfig = Object.assign({}, this.props.config)
|
||||
newConfig.editor.type = type
|
||||
@@ -361,7 +368,9 @@ class MarkdownNoteDetail extends React.Component {
|
||||
value={note.content}
|
||||
storageKey={note.storage}
|
||||
noteKey={note.key}
|
||||
linesHighlighted={note.linesHighlighted}
|
||||
onChange={this.handleUpdateContent.bind(this)}
|
||||
isLocked={this.state.isLocked}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
/>
|
||||
} else {
|
||||
@@ -371,6 +380,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
value={note.content}
|
||||
storageKey={note.storage}
|
||||
noteKey={note.key}
|
||||
linesHighlighted={note.linesHighlighted}
|
||||
onChange={this.handleUpdateContent.bind(this)}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
/>
|
||||
@@ -433,6 +443,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={this.handleUpdateTag.bind(this)}
|
||||
coloredTags={config.coloredTags}
|
||||
/>
|
||||
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import _ from 'lodash'
|
||||
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import FullscreenButton from './FullscreenButton'
|
||||
import TrashButton from './TrashButton'
|
||||
import RestoreButton from './RestoreButton'
|
||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||
@@ -48,7 +49,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
note: Object.assign({
|
||||
description: ''
|
||||
}, props.note, {
|
||||
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||
snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,8 +77,9 @@ class SnippetNoteDetail extends React.Component {
|
||||
const nextNote = Object.assign({
|
||||
description: ''
|
||||
}, nextProps.note, {
|
||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
|
||||
})
|
||||
|
||||
this.setState({
|
||||
snippetIndex: 0,
|
||||
note: nextNote
|
||||
@@ -410,6 +412,8 @@ class SnippetNoteDetail extends React.Component {
|
||||
return (e) => {
|
||||
const snippets = this.state.note.snippets.slice()
|
||||
snippets[index].content = this.refs['code-' + index].value
|
||||
snippets[index].linesHighlighted = e.options.linesHighlighted
|
||||
|
||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||
this.setState(state => ({
|
||||
note: state.note
|
||||
@@ -596,13 +600,16 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
addSnippet () {
|
||||
const { config } = this.props
|
||||
const { config: { editor: { snippetDefaultLanguage } } } = this.props
|
||||
const { note } = this.state
|
||||
|
||||
const defaultLanguage = snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
|
||||
|
||||
note.snippets = note.snippets.concat([{
|
||||
name: '',
|
||||
mode: config.editor.snippetDefaultLanguage || 'text',
|
||||
content: ''
|
||||
mode: defaultLanguage,
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}])
|
||||
const snippetIndex = note.snippets.length - 1
|
||||
|
||||
@@ -645,11 +652,18 @@ class SnippetNoteDetail extends React.Component {
|
||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||
}
|
||||
|
||||
showWarning () {
|
||||
showWarning (e, msg) {
|
||||
const warningMessage = (msg) => ({
|
||||
'export-txt': 'Text export',
|
||||
'export-md': 'Markdown export',
|
||||
'export-html': 'HTML export',
|
||||
'print': 'Print'
|
||||
})[msg]
|
||||
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: i18n.__('Sorry!'),
|
||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
||||
buttons: [i18n.__('OK')]
|
||||
})
|
||||
}
|
||||
@@ -661,6 +675,8 @@ class SnippetNoteDetail extends React.Component {
|
||||
const storageKey = note.storage
|
||||
const folderKey = note.folder
|
||||
|
||||
const autoDetect = config.editor.snippetDefaultLanguage === 'Auto Detect'
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
@@ -685,10 +701,6 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
const viewList = note.snippets.map((snippet, index) => {
|
||||
const isActive = this.state.snippetIndex === index
|
||||
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
return <div styleName='tabView'
|
||||
key={index}
|
||||
style={{zIndex: isActive ? 5 : 4}}
|
||||
@@ -697,26 +709,34 @@ class SnippetNoteDetail extends React.Component {
|
||||
? <MarkdownEditor styleName='tabView-content'
|
||||
value={snippet.content}
|
||||
config={config}
|
||||
linesHighlighted={snippet.linesHighlighted}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
storageKey={storageKey}
|
||||
/>
|
||||
: <CodeEditor styleName='tabView-content'
|
||||
mode={snippet.mode}
|
||||
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
|
||||
value={snippet.content}
|
||||
linesHighlighted={snippet.linesHighlighted}
|
||||
theme={config.editor.theme}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
keyMap={config.editor.keyMap}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
autoDetect={autoDetect}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -772,6 +792,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
coloredTags={config.coloredTags}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
@@ -780,11 +801,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
|
||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
||||
</button>
|
||||
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
|
||||
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
|
||||
@@ -800,7 +817,9 @@ class SnippetNoteDetail extends React.Component {
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
exportAsHtml={this.showWarning}
|
||||
type={note.type}
|
||||
print={this.showWarning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
.tabList
|
||||
absolute left right
|
||||
top 55px
|
||||
top 70px
|
||||
height 30px
|
||||
display flex
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
@@ -57,6 +57,9 @@
|
||||
.tabList .tabButton
|
||||
navWhiteButtonColor()
|
||||
width 30px
|
||||
border-left 1px solid $ui-borderColor
|
||||
border-top 1px solid $ui-borderColor
|
||||
border-right 1px solid $ui-borderColor
|
||||
|
||||
.tabView
|
||||
absolute left right bottom
|
||||
@@ -98,17 +101,34 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="white"]
|
||||
body[data-theme="white"], body[data-theme="default"]
|
||||
.root
|
||||
box-shadow $note-detail-box-shadow
|
||||
border none
|
||||
|
||||
.tabButton
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||
color $ui-text-color
|
||||
transition 0.15s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
|
||||
.tabList .tabButton
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
|
||||
.tabButton
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
color $ui-dark-text-color
|
||||
transition 0.15s
|
||||
|
||||
.body
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
@@ -118,7 +138,6 @@ body[data-theme="dark"]
|
||||
border 1px solid $ui-dark-borderColor
|
||||
|
||||
.tabList
|
||||
background-color $ui-button--active-backgroundColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.tabList .list
|
||||
@@ -150,6 +169,15 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-text-color
|
||||
border 1px solid $ui-solarized-dark-borderColor
|
||||
|
||||
.tabList .tabButton
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
.tabButton
|
||||
&:hover
|
||||
color $ui-solarized-dark-button--active-color
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
transition 0.15s
|
||||
|
||||
.tabList
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
@@ -167,6 +195,14 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
border 1px solid $ui-monokai-borderColor
|
||||
|
||||
.tabList .tabButton
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.tabButton
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.tabList
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
@@ -184,6 +220,14 @@ body[data-theme="dracula"]
|
||||
color $ui-dracula-text-color
|
||||
border 1px solid $ui-dracula-borderColor
|
||||
|
||||
.tabList .tabButton
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.tabButton
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
.tabList
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
@@ -54,7 +54,7 @@ class StarButton extends React.Component {
|
||||
: '../resources/icon/icon-star.svg'
|
||||
}
|
||||
/>
|
||||
<span styleName='tooltip'>{i18n.__('Star')}</span>
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Star')}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
right 103px
|
||||
width 70px
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
transition 0.15s
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import invertColor from 'invert-color'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TagSelect.styl'
|
||||
import _ from 'lodash'
|
||||
@@ -45,8 +46,14 @@ class TagSelect extends React.Component {
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
value.push(newTag)
|
||||
value = _.uniq(value)
|
||||
|
||||
if (!_.includes(value, newTag)) {
|
||||
value.push(newTag)
|
||||
}
|
||||
|
||||
if (this.props.saveTagsAlphabetically) {
|
||||
value = _.sortBy(value)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
newTag: ''
|
||||
@@ -179,19 +186,34 @@ class TagSelect extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, className, showTagsAlphabetically } = this.props
|
||||
const { value, className, showTagsAlphabetically, coloredTags } = this.props
|
||||
|
||||
const tagList = _.isArray(value)
|
||||
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
|
||||
const wrapperStyle = {}
|
||||
const textStyle = {}
|
||||
const BLACK = '#333333'
|
||||
const WHITE = '#f1f1f1'
|
||||
const color = coloredTags[tag]
|
||||
const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE })
|
||||
let iconRemove = '../resources/icon/icon-x.svg'
|
||||
if (color) {
|
||||
wrapperStyle.backgroundColor = color
|
||||
textStyle.color = invertedColor
|
||||
}
|
||||
if (invertedColor === WHITE) {
|
||||
iconRemove = '../resources/icon/icon-x-light.svg'
|
||||
}
|
||||
return (
|
||||
<span styleName='tag'
|
||||
key={tag}
|
||||
style={wrapperStyle}
|
||||
>
|
||||
<span styleName='tag-label' onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
|
||||
<span styleName='tag-label' style={textStyle} onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
|
||||
<button styleName='tag-removeButton'
|
||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
|
||||
>
|
||||
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
|
||||
<img className='tag-removeButton-icon' src={iconRemove} width='8px' />
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
@@ -240,7 +262,8 @@ TagSelect.contextTypes = {
|
||||
TagSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func
|
||||
onChange: PropTypes.func,
|
||||
coloredTags: PropTypes.object
|
||||
}
|
||||
|
||||
export default CSSModules(TagSelect, styles)
|
||||
|
||||
@@ -3,19 +3,18 @@
|
||||
align-items center
|
||||
user-select none
|
||||
vertical-align middle
|
||||
width 100%
|
||||
overflow-x scroll
|
||||
width 96%
|
||||
overflow-x auto
|
||||
white-space nowrap
|
||||
margin-top 31px
|
||||
top 50px
|
||||
position absolute
|
||||
|
||||
.root::-webkit-scrollbar
|
||||
display none
|
||||
&::-webkit-scrollbar
|
||||
height 8px
|
||||
|
||||
.tag
|
||||
display flex
|
||||
align-items center
|
||||
margin 0px 2px
|
||||
margin 0px 2px 2px
|
||||
padding 2px 4px
|
||||
background-color alpha($ui-tag-backgroundColor, 3%)
|
||||
border-radius 4px
|
||||
|
||||
@@ -14,7 +14,7 @@ const ToggleModeButton = ({
|
||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||
</div>
|
||||
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
left -8px
|
||||
width 70px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
|
||||
@@ -11,7 +11,7 @@ const TrashButton = ({
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
right 46px
|
||||
|
||||
.control-trashButton--in-trash
|
||||
top 60px
|
||||
topBarButtonRight()
|
||||
|
||||
@@ -96,12 +96,14 @@ class Main extends React.Component {
|
||||
{
|
||||
name: 'example.html',
|
||||
mode: 'html',
|
||||
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
|
||||
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
|
||||
linesHighlighted: []
|
||||
},
|
||||
{
|
||||
name: 'example.js',
|
||||
mode: 'javascript',
|
||||
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
|
||||
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)",
|
||||
linesHighlighted: []
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -170,10 +172,21 @@ class Main extends React.Component {
|
||||
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
||||
|
||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||
eventEmitter.on('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
|
||||
eventEmitter.off('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
|
||||
}
|
||||
|
||||
toggleMenuBarVisible () {
|
||||
const { config } = this.props
|
||||
const { ui } = config
|
||||
|
||||
const newUI = Object.assign(ui, {showMenuBar: !ui.showMenuBar})
|
||||
const newConfig = Object.assign(config, newUI)
|
||||
ConfigManager.set(newConfig)
|
||||
}
|
||||
|
||||
handleLeftSlideMouseDown (e) {
|
||||
@@ -234,8 +247,8 @@ class Main extends React.Component {
|
||||
if (this.state.isRightSliderFocused) {
|
||||
const offset = this.refs.body.getBoundingClientRect().left
|
||||
let newListWidth = e.pageX - offset
|
||||
if (newListWidth < 10) {
|
||||
newListWidth = 10
|
||||
if (newListWidth < 180) {
|
||||
newListWidth = 180
|
||||
} else if (newListWidth > 600) {
|
||||
newListWidth = 600
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import debounceRender from 'react-debounce-render'
|
||||
import styles from './NoteList.styl'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
@@ -64,13 +63,14 @@ class NoteList extends React.Component {
|
||||
this.focusHandler = () => {
|
||||
this.refs.list.focus()
|
||||
}
|
||||
this.alertIfSnippetHandler = () => {
|
||||
this.alertIfSnippet()
|
||||
this.alertIfSnippetHandler = (event, msg) => {
|
||||
this.alertIfSnippet(msg)
|
||||
}
|
||||
this.importFromFileHandler = this.importFromFile.bind(this)
|
||||
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
||||
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
||||
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
||||
this.cloneNote = this.cloneNote.bind(this)
|
||||
this.deleteNote = this.deleteNote.bind(this)
|
||||
this.focusNote = this.focusNote.bind(this)
|
||||
this.pinToTop = this.pinToTop.bind(this)
|
||||
@@ -96,6 +96,7 @@ class NoteList extends React.Component {
|
||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||
ee.on('list:next', this.selectNextNoteHandler)
|
||||
ee.on('list:prior', this.selectPriorNoteHandler)
|
||||
ee.on('list:clone', this.cloneNote)
|
||||
ee.on('list:focus', this.focusHandler)
|
||||
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||
ee.on('import:file', this.importFromFileHandler)
|
||||
@@ -118,6 +119,7 @@ class NoteList extends React.Component {
|
||||
|
||||
ee.off('list:next', this.selectNextNoteHandler)
|
||||
ee.off('list:prior', this.selectPriorNoteHandler)
|
||||
ee.off('list:clone', this.cloneNote)
|
||||
ee.off('list:focus', this.focusHandler)
|
||||
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||
ee.off('import:file', this.importFromFileHandler)
|
||||
@@ -173,16 +175,15 @@ class NoteList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
focusNote (selectedNoteKeys, noteKey) {
|
||||
focusNote (selectedNoteKeys, noteKey, pathname) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys
|
||||
})
|
||||
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
pathname,
|
||||
query: {
|
||||
key: noteKey
|
||||
}
|
||||
@@ -201,6 +202,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
const { location } = this.props
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
|
||||
@@ -217,7 +219,7 @@ class NoteList extends React.Component {
|
||||
selectedNoteKeys.push(priorNoteKey)
|
||||
}
|
||||
|
||||
this.focusNote(selectedNoteKeys, priorNoteKey)
|
||||
this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
@@ -228,6 +230,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
const { location } = this.props
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
const isTargetLastNote = targetIndex === this.notes.length - 1
|
||||
@@ -250,7 +253,7 @@ class NoteList extends React.Component {
|
||||
selectedNoteKeys.push(nextNoteKey)
|
||||
}
|
||||
|
||||
this.focusNote(selectedNoteKeys, nextNoteKey)
|
||||
this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
@@ -262,7 +265,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
const selectedNoteKeys = [noteHash]
|
||||
this.focusNote(selectedNoteKeys, noteHash)
|
||||
this.focusNote(selectedNoteKeys, noteHash, '/home')
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
@@ -276,12 +279,6 @@ class NoteList extends React.Component {
|
||||
ee.emit('top:new-note')
|
||||
}
|
||||
|
||||
// D key
|
||||
if (e.keyCode === 68) {
|
||||
e.preventDefault()
|
||||
this.deleteNote()
|
||||
}
|
||||
|
||||
// E key
|
||||
if (e.keyCode === 69) {
|
||||
e.preventDefault()
|
||||
@@ -494,14 +491,21 @@ class NoteList extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
alertIfSnippet () {
|
||||
alertIfSnippet (msg) {
|
||||
const warningMessage = (msg) => ({
|
||||
'export-txt': 'Text export',
|
||||
'export-md': 'Markdown export',
|
||||
'export-html': 'HTML export',
|
||||
'print': 'Print'
|
||||
})[msg]
|
||||
|
||||
const targetIndex = this.getTargetIndex()
|
||||
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: i18n.__('Sorry!'),
|
||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||
buttons: [i18n.__('OK'), i18n.__('Cancel')]
|
||||
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
||||
buttons: [i18n.__('OK')]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -652,14 +656,18 @@ class NoteList extends React.Component {
|
||||
})
|
||||
)
|
||||
.then((data) => {
|
||||
data.forEach((item) => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: item.storageKey,
|
||||
noteKey: item.noteKey
|
||||
const dispatchHandler = () => {
|
||||
data.forEach((item) => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: item.storageKey,
|
||||
noteKey: item.noteKey
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
ee.once('list:next', dispatchHandler)
|
||||
})
|
||||
.then(() => ee.emit('list:next'))
|
||||
.catch((err) => {
|
||||
console.error('Cannot Delete note: ' + err)
|
||||
})
|
||||
@@ -683,6 +691,7 @@ class NoteList extends React.Component {
|
||||
})
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||
})
|
||||
.then(() => ee.emit('list:next'))
|
||||
.catch((err) => {
|
||||
console.error('Notes could not go to trash: ' + err)
|
||||
})
|
||||
@@ -706,7 +715,8 @@ class NoteList extends React.Component {
|
||||
type: firstNote.type,
|
||||
folder: folder.key,
|
||||
title: firstNote.title + ' ' + i18n.__('copy'),
|
||||
content: firstNote.content
|
||||
content: firstNote.content,
|
||||
linesHighlighted: firstNote.linesHighlighted
|
||||
})
|
||||
.then((note) => {
|
||||
attachmentManagement.cloneAttachments(firstNote, note)
|
||||
@@ -1042,6 +1052,7 @@ class NoteList extends React.Component {
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
viewType={viewType}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
coloredTags={config.coloredTags}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1124,4 +1135,4 @@ NoteList.propTypes = {
|
||||
})
|
||||
}
|
||||
|
||||
export default debounceRender(CSSModules(NoteList, styles))
|
||||
export default CSSModules(NoteList, styles)
|
||||
|
||||
@@ -25,7 +25,8 @@ class StorageItem extends React.Component {
|
||||
const { storage } = this.props
|
||||
|
||||
this.state = {
|
||||
isOpen: !!storage.isOpen
|
||||
isOpen: !!storage.isOpen,
|
||||
draggedOver: null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +205,20 @@ class StorageItem extends React.Component {
|
||||
folderKey: data.folderKey,
|
||||
fileType: data.fileType
|
||||
})
|
||||
return data
|
||||
})
|
||||
.then(data => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: 'Exported to "' + data.exportDir + '"'
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
err ? err.message || err : 'Unexpected error during export'
|
||||
)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -231,14 +246,20 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleDragEnter (e) {
|
||||
e.dataTransfer.setData('defaultColor', e.target.style.backgroundColor)
|
||||
e.target.style.backgroundColor = 'rgba(129, 130, 131, 0.08)'
|
||||
handleDragEnter (e, key) {
|
||||
e.preventDefault()
|
||||
if (this.state.draggedOver === key) { return }
|
||||
this.setState({
|
||||
draggedOver: key
|
||||
})
|
||||
}
|
||||
|
||||
handleDragLeave (e) {
|
||||
e.target.style.opacity = '1'
|
||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
||||
e.preventDefault()
|
||||
if (this.state.draggedOver === null) { return }
|
||||
this.setState({
|
||||
draggedOver: null
|
||||
})
|
||||
}
|
||||
|
||||
dropNote (storage, folder, dispatch, location, noteData) {
|
||||
@@ -263,8 +284,12 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
|
||||
handleDrop (e, storage, folder, dispatch, location) {
|
||||
e.target.style.opacity = '1'
|
||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
||||
e.preventDefault()
|
||||
if (this.state.draggedOver !== null) {
|
||||
this.setState({
|
||||
draggedOver: null
|
||||
})
|
||||
}
|
||||
const noteData = JSON.parse(e.dataTransfer.getData('note'))
|
||||
this.dropNote(storage, folder, dispatch, location, noteData)
|
||||
}
|
||||
@@ -291,16 +316,22 @@ class StorageItem extends React.Component {
|
||||
<SortableStorageItemChild
|
||||
key={folder.key}
|
||||
index={index}
|
||||
isActive={isActive}
|
||||
isActive={isActive || folder.key === this.state.draggedOver}
|
||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||
folderName={folder.name}
|
||||
folderColor={folder.color}
|
||||
isFolded={isFolded}
|
||||
noteCount={noteCount}
|
||||
handleDrop={(e) => this.handleDrop(e, storage, folder, dispatch, location)}
|
||||
handleDragEnter={this.handleDragEnter}
|
||||
handleDragLeave={this.handleDragLeave}
|
||||
handleDrop={(e) => {
|
||||
this.handleDrop(e, storage, folder, dispatch, location)
|
||||
}}
|
||||
handleDragEnter={(e) => {
|
||||
this.handleDragEnter(e, folder.key)
|
||||
}}
|
||||
handleDragLeave={(e) => {
|
||||
this.handleDragLeave(e, folder)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ import i18n from 'browser/lib/i18n'
|
||||
import context from 'browser/lib/context'
|
||||
import { remote } from 'electron'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import ColorPicker from 'browser/components/ColorPicker'
|
||||
|
||||
function matchActiveTags (tags, activeTags) {
|
||||
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
||||
@@ -27,6 +28,22 @@ function matchActiveTags (tags, activeTags) {
|
||||
|
||||
class SideNav extends React.Component {
|
||||
// TODO: should not use electron stuff v0.7
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
colorPicker: {
|
||||
show: false,
|
||||
color: null,
|
||||
tagName: null,
|
||||
targetRect: null
|
||||
}
|
||||
}
|
||||
|
||||
this.dismissColorPicker = this.dismissColorPicker.bind(this)
|
||||
this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this)
|
||||
this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
|
||||
@@ -104,9 +121,64 @@ class SideNav extends React.Component {
|
||||
click: this.deleteTag.bind(this, tag)
|
||||
})
|
||||
|
||||
menu.push({
|
||||
label: i18n.__('Customize Color'),
|
||||
click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect())
|
||||
})
|
||||
|
||||
context.popup(menu)
|
||||
}
|
||||
|
||||
dismissColorPicker () {
|
||||
this.setState({
|
||||
colorPicker: {
|
||||
show: false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
displayColorPicker (tagName, rect) {
|
||||
const { config } = this.props
|
||||
this.setState({
|
||||
colorPicker: {
|
||||
show: true,
|
||||
color: config.coloredTags[tagName],
|
||||
tagName,
|
||||
targetRect: rect
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleColorPickerConfirm (color) {
|
||||
const { dispatch, config: {coloredTags} } = this.props
|
||||
const { colorPicker: { tagName } } = this.state
|
||||
const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex})
|
||||
|
||||
const config = { coloredTags: newColoredTags }
|
||||
ConfigManager.set(config)
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config
|
||||
})
|
||||
this.dismissColorPicker()
|
||||
}
|
||||
|
||||
handleColorPickerReset () {
|
||||
const { dispatch, config: {coloredTags} } = this.props
|
||||
const { colorPicker: { tagName } } = this.state
|
||||
const newColoredTags = Object.assign({}, coloredTags)
|
||||
|
||||
delete newColoredTags[tagName]
|
||||
|
||||
const config = { coloredTags: newColoredTags }
|
||||
ConfigManager.set(config)
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config
|
||||
})
|
||||
this.dismissColorPicker()
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
const { dispatch, config } = this.props
|
||||
|
||||
@@ -207,6 +279,7 @@ class SideNav extends React.Component {
|
||||
|
||||
tagListComponent () {
|
||||
const { data, location, config } = this.props
|
||||
const { colorPicker } = this.state
|
||||
const activeTags = this.getActiveTags(location.pathname)
|
||||
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
||||
let tagList = _.sortBy(data.tagNoteMap.map(
|
||||
@@ -237,10 +310,11 @@ class SideNav extends React.Component {
|
||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
||||
isActive={this.getTagActive(location.pathname, tag.name)}
|
||||
isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)}
|
||||
isRelated={tag.related}
|
||||
key={tag.name}
|
||||
count={tag.size}
|
||||
color={config.coloredTags[tag.name]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -333,6 +407,7 @@ class SideNav extends React.Component {
|
||||
|
||||
render () {
|
||||
const { data, location, config, dispatch } = this.props
|
||||
const { colorPicker: colorPickerState } = this.state
|
||||
|
||||
const isFolded = config.isSideNavFolded
|
||||
|
||||
@@ -349,6 +424,20 @@ class SideNav extends React.Component {
|
||||
useDragHandle
|
||||
/>
|
||||
})
|
||||
|
||||
let colorPicker
|
||||
if (colorPickerState.show) {
|
||||
colorPicker = (
|
||||
<ColorPicker
|
||||
color={colorPickerState.color}
|
||||
targetRect={colorPickerState.targetRect}
|
||||
onConfirm={this.handleColorPickerConfirm}
|
||||
onCancel={this.dismissColorPicker}
|
||||
onReset={this.handleColorPickerReset}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const style = {}
|
||||
if (!isFolded) style.width = this.props.width
|
||||
const isTagActive = location.pathname.match(/tag/)
|
||||
@@ -368,6 +457,7 @@ class SideNav extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
{this.SideNavComponent(isFolded, storageList)}
|
||||
{colorPicker}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import _ from 'lodash'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import NewNoteButton from 'browser/main/NewNoteButton'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
class TopBar extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -25,6 +26,10 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
this.codeInitHandler = this.handleCodeInit.bind(this)
|
||||
|
||||
this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, {
|
||||
maxWait: 1000 / 8
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -94,7 +99,6 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
handleKeyUp (e) {
|
||||
const { router } = this.context
|
||||
// reset states
|
||||
this.setState({
|
||||
isConfirmTranslation: false
|
||||
@@ -106,21 +110,21 @@ class TopBar extends React.Component {
|
||||
isConfirmTranslation: true
|
||||
})
|
||||
const keyword = this.refs.searchInput.value
|
||||
router.push(`/searched/${encodeURIComponent(keyword)}`)
|
||||
this.setState({
|
||||
search: keyword
|
||||
})
|
||||
this.updateKeyword(keyword)
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
const { router } = this.context
|
||||
const keyword = this.refs.searchInput.value
|
||||
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
||||
router.push(`/searched/${encodeURIComponent(keyword)}`)
|
||||
const keyword = this.refs.searchInput.value
|
||||
this.updateKeyword(keyword)
|
||||
} else {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
updateKeyword (keyword) {
|
||||
this.context.router.push(`/searched/${encodeURIComponent(keyword)}`)
|
||||
this.setState({
|
||||
search: keyword
|
||||
})
|
||||
|
||||
@@ -97,6 +97,7 @@ modalBackColor = white
|
||||
|
||||
|
||||
body[data-theme="dark"]
|
||||
background-color $ui-dark-backgroundColor
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.3)
|
||||
.ModalBase
|
||||
@@ -148,6 +149,7 @@ body[data-theme="dark"]
|
||||
z-index modalZIndex + 5
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.3)
|
||||
.ModalBase
|
||||
@@ -157,6 +159,7 @@ body[data-theme="solarized-dark"]
|
||||
color: $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
background-color $ui-monokai-backgroundColor
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.3)
|
||||
.ModalBase
|
||||
@@ -166,6 +169,7 @@ body[data-theme="monokai"]
|
||||
color: $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
background-color $ui-dracula-backgroundColor
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.3)
|
||||
.ModalBase
|
||||
|
||||
@@ -25,25 +25,31 @@ export const DEFAULT_CONFIG = {
|
||||
hotkey: {
|
||||
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
||||
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
||||
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
|
||||
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
|
||||
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
|
||||
toggleMenuBar: 'Alt'
|
||||
},
|
||||
ui: {
|
||||
language: 'en',
|
||||
theme: 'default',
|
||||
showCopyNotification: true,
|
||||
disableDirectWrite: false,
|
||||
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||
defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||
showMenuBar: false
|
||||
},
|
||||
editor: {
|
||||
theme: 'base16-light',
|
||||
keyMap: 'sublime',
|
||||
fontSize: '14',
|
||||
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
|
||||
fontFamily: win ? 'Consolas' : 'Monaco',
|
||||
indentType: 'space',
|
||||
indentSize: '2',
|
||||
enableRulers: false,
|
||||
rulers: [80, 120],
|
||||
displayLineNumbers: true,
|
||||
matchingPairs: '()[]{}\'\'""$$**``',
|
||||
matchingTriples: '```"""\'\'\'',
|
||||
explodingPairs: '[]{}``$$',
|
||||
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
||||
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
|
||||
scrollPastEnd: false,
|
||||
@@ -52,7 +58,8 @@ export const DEFAULT_CONFIG = {
|
||||
enableTableEditor: false,
|
||||
enableFrontMatterTitle: true,
|
||||
frontMatterTitleField: 'title',
|
||||
spellcheck: false
|
||||
spellcheck: false,
|
||||
enableSmartPaste: false
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
@@ -81,7 +88,8 @@ export const DEFAULT_CONFIG = {
|
||||
token: '',
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
coloredTags: {}
|
||||
}
|
||||
|
||||
function validate (config) {
|
||||
@@ -203,7 +211,7 @@ function assignConfigValues (originalConfig, rcConfig) {
|
||||
function rewriteHotkey (config) {
|
||||
const keys = [...Object.keys(config.hotkey)]
|
||||
keys.forEach(key => {
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Cmd\s/g, 'Command ')
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
|
||||
})
|
||||
return config
|
||||
|
||||
@@ -241,7 +241,15 @@ function migrateAttachments (markdownContent, storagePath, noteKey) {
|
||||
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
|
||||
*/
|
||||
function fixLocalURLS (renderedHTML, storagePath) {
|
||||
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) {
|
||||
/*
|
||||
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
|
||||
|
||||
- `STORAGE_FOLDER_PLACEHOLDER` will match `:storage`
|
||||
- `(?:(?:\\\/|%5C)[-.\\w]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`
|
||||
- `(?:\\\/|%5C)[-.\\w]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg`
|
||||
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
|
||||
*/
|
||||
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
|
||||
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
|
||||
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
|
||||
})
|
||||
@@ -270,7 +278,7 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||
let promise
|
||||
if (dropEvent.dataTransfer.files.length > 0) {
|
||||
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
|
||||
if (file['type'].startsWith('image')) {
|
||||
if (file['type'].startsWith('image') && !file['type'].endsWith('gif')) {
|
||||
return fixRotate(file)
|
||||
.then(data => copyAttachment({type: 'base64', data: data, sourceFilePath: file.path}, storageKey, noteKey)
|
||||
.then(fileName => ({
|
||||
@@ -330,7 +338,7 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||
* @param {String} noteKey Key of the current note
|
||||
* @param {DataTransferItem} dataTransferItem Part of the past-event
|
||||
*/
|
||||
function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
|
||||
function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
|
||||
if (!codeEditor) {
|
||||
throw new Error('codeEditor has to be given')
|
||||
}
|
||||
@@ -367,6 +375,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
|
||||
reader.readAsDataURL(blob)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
|
||||
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
|
||||
* @param {String} storageKey Key of the current storage
|
||||
* @param {String} noteKey Key of the current note
|
||||
* @param {NativeImage} image The native image
|
||||
*/
|
||||
function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
|
||||
if (!codeEditor) {
|
||||
throw new Error('codeEditor has to be given')
|
||||
}
|
||||
if (!storageKey) {
|
||||
throw new Error('storageKey has to be given')
|
||||
}
|
||||
|
||||
if (!noteKey) {
|
||||
throw new Error('noteKey has to be given')
|
||||
}
|
||||
if (!image) {
|
||||
throw new Error('image has to be given')
|
||||
}
|
||||
|
||||
const targetStorage = findStorage.findStorage(storageKey)
|
||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||
|
||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||
|
||||
const imageName = `${uniqueSlug()}.png`
|
||||
const imagePath = path.join(destinationDir, imageName)
|
||||
|
||||
const binaryData = image.toPNG()
|
||||
fs.writeFileSync(imagePath, binaryData, 'binary')
|
||||
|
||||
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
|
||||
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
|
||||
codeEditor.insertAttachmentMd(imageMd)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns all attachment paths of the given markdown
|
||||
* @param {String} markdownContent content in which the attachment paths should be found
|
||||
@@ -434,7 +480,14 @@ function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
|
||||
* @returns {String} Input without the references
|
||||
*/
|
||||
function removeStorageAndNoteReferences (input, noteKey) {
|
||||
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
|
||||
return input.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'), function (match) {
|
||||
const temp = match
|
||||
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep)
|
||||
.replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep)
|
||||
.replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep)
|
||||
.replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep)
|
||||
return temp.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -589,7 +642,8 @@ module.exports = {
|
||||
fixLocalURLS,
|
||||
generateAttachmentMarkdown,
|
||||
handleAttachmentDrop,
|
||||
handlePastImageEvent,
|
||||
handlePasteImageEvent,
|
||||
handlePasteNativeImage,
|
||||
getAttachmentsInMarkdownContent,
|
||||
getAbsolutePathsOfAttachmentsInContent,
|
||||
removeStorageAndNoteReferences,
|
||||
|
||||
@@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) {
|
||||
const dstFolder = path.dirname(dstPath)
|
||||
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
||||
|
||||
const input = fs.createReadStream(srcPath)
|
||||
const input = fs.createReadStream(decodeURI(srcPath))
|
||||
const output = fs.createWriteStream(dstPath)
|
||||
|
||||
output.on('error', reject)
|
||||
|
||||
@@ -16,6 +16,7 @@ function validateInput (input) {
|
||||
switch (input.type) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
if (!_.isString(input.content)) input.content = ''
|
||||
if (!_.isArray(input.linesHighlighted)) input.linesHighlighted = []
|
||||
break
|
||||
case 'SNIPPET_NOTE':
|
||||
if (!_.isString(input.description)) input.description = ''
|
||||
@@ -23,7 +24,8 @@ function validateInput (input) {
|
||||
input.snippets = [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}]
|
||||
}
|
||||
break
|
||||
|
||||
@@ -9,7 +9,8 @@ function createSnippet (snippetFile) {
|
||||
id: crypto.randomBytes(16).toString('hex'),
|
||||
name: 'Unnamed snippet',
|
||||
prefix: [],
|
||||
content: ''
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}
|
||||
fetchSnippet(null, snippetFile).then((snippets) => {
|
||||
snippets.push(newSnippet)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import resolveStorageData from './resolveStorageData'
|
||||
import resolveStorageNotes from './resolveStorageNotes'
|
||||
import exportNote from './exportNote'
|
||||
import filenamify from 'filenamify'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
@@ -45,9 +45,9 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
||||
|
||||
notes
|
||||
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
||||
.forEach(snippet => {
|
||||
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
|
||||
fs.writeFileSync(notePath, snippet.content)
|
||||
.forEach(note => {
|
||||
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
|
||||
exportNote(note.key, storage.path, note.content, notePath, null)
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,27 +4,43 @@ import { findStorage } from 'browser/lib/findStorage'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const attachmentManagement = require('./attachmentManagement')
|
||||
|
||||
/**
|
||||
* Export note together with images
|
||||
* Export note together with attachments
|
||||
*
|
||||
* If images is stored in the storage, creates 'images' subfolder in target directory
|
||||
* and copies images to it. Changes links to images in the content of the note
|
||||
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory
|
||||
* and copies attachments to it. Changes links to images in the content of the note
|
||||
*
|
||||
* @param {String} nodeKey key of the node that should be exported
|
||||
* @param {String} storageKey or storage path
|
||||
* @param {String} noteContent Content to export
|
||||
* @param {String} targetPath Path to exported file
|
||||
* @param {function} outputFormatter
|
||||
* @return {Promise.<*[]>}
|
||||
*/
|
||||
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
||||
function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) {
|
||||
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
||||
const exportTasks = []
|
||||
|
||||
if (!storagePath) {
|
||||
throw new Error('Storage path is not found')
|
||||
}
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
noteContent,
|
||||
storagePath
|
||||
)
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
|
||||
let exportedData = noteContent
|
||||
let exportedData = attachmentManagement.removeStorageAndNoteReferences(
|
||||
noteContent,
|
||||
nodeKey
|
||||
)
|
||||
|
||||
if (outputFormatter) {
|
||||
exportedData = outputFormatter(exportedData, exportTasks)
|
||||
|
||||
@@ -4,6 +4,7 @@ const resolveStorageData = require('./resolveStorageData')
|
||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||
const consts = require('browser/lib/consts')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const CSON = require('@rokt33r/season')
|
||||
/**
|
||||
* @return {Object} all storages and notes
|
||||
@@ -19,11 +20,14 @@ const CSON = require('@rokt33r/season')
|
||||
* 2. legacy
|
||||
* 3. empty directory
|
||||
*/
|
||||
|
||||
function init () {
|
||||
const fetchStorages = function () {
|
||||
let rawStorages
|
||||
try {
|
||||
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
|
||||
// Remove storages who's location is inaccesible.
|
||||
rawStorages = rawStorages.filter(storage => fs.existsSync(storage.path))
|
||||
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse cached data from localStorage', e)
|
||||
@@ -36,6 +40,7 @@ function init () {
|
||||
|
||||
const fetchNotes = function (storages) {
|
||||
const findNotesFromEachStorage = storages
|
||||
.filter(storage => fs.existsSync(storage.path))
|
||||
.map((storage) => {
|
||||
return resolveStorageNotes(storage)
|
||||
.then((notes) => {
|
||||
@@ -51,7 +56,11 @@ function init () {
|
||||
}
|
||||
})
|
||||
if (unknownCount > 0) {
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
try {
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
} catch (e) {
|
||||
console.log('Error writting boostnote.json: ' + e + ' from init.js')
|
||||
}
|
||||
}
|
||||
return notes
|
||||
})
|
||||
|
||||
@@ -69,7 +69,8 @@ function importAll (storage, data) {
|
||||
isStarred: false,
|
||||
title: article.title,
|
||||
content: '# ' + article.title + '\n\n' + article.content,
|
||||
key: noteKey
|
||||
key: noteKey,
|
||||
linesHighlighted: article.linesHighlighted
|
||||
}
|
||||
notes.push(newNote)
|
||||
} else {
|
||||
@@ -87,7 +88,8 @@ function importAll (storage, data) {
|
||||
snippets: [{
|
||||
name: article.mode,
|
||||
mode: article.mode,
|
||||
content: article.content
|
||||
content: article.content,
|
||||
linesHighlighted: article.linesHighlighted
|
||||
}]
|
||||
}
|
||||
notes.push(newNote)
|
||||
|
||||
@@ -39,6 +39,9 @@ function validateInput (input) {
|
||||
if (input.content != null) {
|
||||
if (!_.isString(input.content)) validatedInput.content = ''
|
||||
else validatedInput.content = input.content
|
||||
|
||||
if (!_.isArray(input.linesHighlighted)) validatedInput.linesHighlighted = []
|
||||
else validatedInput.linesHighlighted = input.linesHighlighted
|
||||
}
|
||||
return validatedInput
|
||||
case 'SNIPPET_NOTE':
|
||||
@@ -51,7 +54,8 @@ function validateInput (input) {
|
||||
validatedInput.snippets = [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}]
|
||||
} else {
|
||||
validatedInput.snippets = input.snippets
|
||||
@@ -96,12 +100,14 @@ function updateNote (storageKey, noteKey, input) {
|
||||
snippets: [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}]
|
||||
}
|
||||
: {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
content: ''
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}
|
||||
noteData.title = ''
|
||||
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')
|
||||
|
||||
@@ -12,7 +12,8 @@ function updateSnippet (snippet, snippetFile) {
|
||||
if (
|
||||
currentSnippet.name === snippet.name &&
|
||||
currentSnippet.prefix === snippet.prefix &&
|
||||
currentSnippet.content === snippet.content
|
||||
currentSnippet.content === snippet.content &&
|
||||
currentSnippet.linesHighlighted === snippet.linesHighlighted
|
||||
) {
|
||||
// if everything is the same then don't write to disk
|
||||
resolve(snippets)
|
||||
@@ -20,6 +21,7 @@ function updateSnippet (snippet, snippetFile) {
|
||||
currentSnippet.name = snippet.name
|
||||
currentSnippet.prefix = snippet.prefix
|
||||
currentSnippet.content = snippet.content
|
||||
currentSnippet.linesHighlighted = (snippet.linesHighlighted)
|
||||
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
||||
if (err) reject(err)
|
||||
resolve(snippets)
|
||||
|
||||
@@ -6,5 +6,8 @@ module.exports = {
|
||||
},
|
||||
'deleteNote': () => {
|
||||
ee.emit('hotkey:deletenote')
|
||||
},
|
||||
'toggleMenuBar': () => {
|
||||
ee.emit('menubar:togglemenubar')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class Crowdfunding extends React.Component {
|
||||
<p>{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('### We believe Meritocracy')}</p>
|
||||
<p>{i18n.__('We think developers who has skill and did 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.__('It sometimes looks like exploitation.')}</p>
|
||||
<p>{i18n.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}</p>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
.folderItem-right-button
|
||||
vertical-align middle
|
||||
height 25px
|
||||
margin-top 2.5px
|
||||
margin-top 2px
|
||||
colorDefaultButton()
|
||||
border-radius 2px
|
||||
border $ui-border
|
||||
|
||||
@@ -79,7 +79,9 @@ class HotkeyTab extends React.Component {
|
||||
config.hotkey = {
|
||||
toggleMain: this.refs.toggleMain.value,
|
||||
toggleMode: this.refs.toggleMode.value,
|
||||
deleteNote: this.refs.deleteNote.value
|
||||
deleteNote: this.refs.deleteNote.value,
|
||||
pasteSmartly: this.refs.pasteSmartly.value,
|
||||
toggleMenuBar: this.refs.toggleMenuBar.value
|
||||
}
|
||||
this.setState({
|
||||
config
|
||||
@@ -127,6 +129,17 @@ class HotkeyTab extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Show/Hide Menu Bar')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
ref='toggleMenuBar'
|
||||
value={config.hotkey.toggleMenuBar}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Toggle Editor Mode')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
@@ -149,6 +162,17 @@ class HotkeyTab extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Paste HTML')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
ref='pasteSmartly'
|
||||
value={config.hotkey.pasteSmartly}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-control'>
|
||||
<button styleName='group-control-leftButton'
|
||||
onClick={(e) => this.handleHintToggleButtonClick(e)}
|
||||
|
||||
@@ -73,6 +73,11 @@ class InfoTab extends React.Component {
|
||||
<div styleName='header--sub'>{i18n.__('Community')}</div>
|
||||
<div styleName='top'>
|
||||
<ul styleName='list'>
|
||||
<li>
|
||||
<a href='https://issuehunt.io/repos/53266139'
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
>{i18n.__('Bounty on IssueHunt')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://boostnote.io/#subscribe'
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
@@ -129,7 +134,7 @@ class InfoTab extends React.Component {
|
||||
>{i18n.__('Development')}</a>{i18n.__(' : Development configurations for Boostnote.')}
|
||||
</li>
|
||||
<li styleName='cc'>
|
||||
{i18n.__('Copyright (C) 2017 - 2018 BoostIO')}
|
||||
{i18n.__('Copyright (C) 2017 - 2019 BoostIO')}
|
||||
</li>
|
||||
<li styleName='cc'>
|
||||
{i18n.__('License: GPL v3')}
|
||||
|
||||
@@ -28,9 +28,9 @@ class SnippetEditor extends React.Component {
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseBrackets: {
|
||||
pairs: '()[]{}\'\'""$$**``',
|
||||
triples: '```"""\'\'\'',
|
||||
explode: '[]{}``$$',
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
},
|
||||
mode: 'null'
|
||||
|
||||
@@ -136,6 +136,9 @@ class SnippetTab extends React.Component {
|
||||
enableRulers={config.editor.enableRulers}
|
||||
rulers={config.editor.rulers}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
onRef={ref => { this.snippetEditor = ref }} />
|
||||
</div>
|
||||
|
||||
@@ -75,6 +75,7 @@ class UiTab extends React.Component {
|
||||
showTagsAlphabetically: this.refs.showTagsAlphabetically.checked,
|
||||
saveTagsAlphabetically: this.refs.saveTagsAlphabetically.checked,
|
||||
enableLiveNoteCounts: this.refs.enableLiveNoteCounts.checked,
|
||||
showMenuBar: this.refs.showMenuBar.checked,
|
||||
disableDirectWrite: this.refs.uiD2w != null
|
||||
? this.refs.uiD2w.checked
|
||||
: false
|
||||
@@ -96,7 +97,11 @@ class UiTab extends React.Component {
|
||||
enableTableEditor: this.refs.enableTableEditor.checked,
|
||||
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
|
||||
frontMatterTitleField: this.refs.frontMatterTitleField.value,
|
||||
spellcheck: this.refs.spellcheck.checked
|
||||
matchingPairs: this.refs.matchingPairs.value,
|
||||
matchingTriples: this.refs.matchingTriples.value,
|
||||
explodingPairs: this.refs.explodingPairs.value,
|
||||
spellcheck: this.refs.spellcheck.checked,
|
||||
enableSmartPaste: this.refs.enableSmartPaste.checked
|
||||
},
|
||||
preview: {
|
||||
fontSize: this.refs.previewFontSize.value,
|
||||
@@ -234,6 +239,16 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.showMenuBar}
|
||||
ref='showMenuBar'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Show menu bar')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -477,6 +492,7 @@ class UiTab extends React.Component {
|
||||
ref='editorSnippetDefaultLanguage'
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
>
|
||||
<option key='Auto Detect' value='Auto Detect'>Auto Detect</option>
|
||||
{
|
||||
_.sortBy(CodeMirror.modeInfo.map(mode => mode.name)).map(name => (<option key={name} value={name}>{name}</option>))
|
||||
}
|
||||
@@ -552,6 +568,18 @@ class UiTab extends React.Component {
|
||||
{i18n.__('Enable smart table editor')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.enableSmartPaste}
|
||||
ref='enableSmartPaste'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Enable HTML paste')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -563,6 +591,48 @@ class UiTab extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Matching character pairs')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
value={this.state.config.editor.matchingPairs}
|
||||
ref='matchingPairs'
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Matching character triples')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
value={this.state.config.editor.matchingTriples}
|
||||
ref='matchingTriples'
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Exploding character pairs')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
value={this.state.config.editor.explodingPairs}
|
||||
ref='explodingPairs'
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-header2'>{i18n.__('Preview')}</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
@@ -590,6 +660,7 @@ class UiTab extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Code Block Theme')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
border none
|
||||
background-color transparent
|
||||
outline none
|
||||
padding 0 4px
|
||||
padding 2px 4px
|
||||
margin 0px 2px 2px
|
||||
font-size 13px
|
||||
height 23px
|
||||
|
||||
ul
|
||||
position fixed
|
||||
|
||||
@@ -240,10 +240,8 @@ navWhiteButtonColor()
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||
transition 0.15s
|
||||
color $ui-text-color
|
||||
&:active, &:active:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-text-color
|
||||
transition 0.15s
|
||||
|
||||
// UI Button
|
||||
|
||||
Reference in New Issue
Block a user