mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-15 18:56:22 +00:00
Merge branch 'master' into fix-mermaid-height
This commit is contained in:
@@ -2,28 +2,40 @@ 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'
|
||||
import crypto from 'crypto'
|
||||
import consts from 'browser/lib/consts'
|
||||
|
||||
import { isMarkdownTitleURL } from 'browser/lib/utils'
|
||||
import styles from '../components/CodeEditor.styl'
|
||||
import fs from 'fs'
|
||||
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'
|
||||
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu
|
||||
import { createTurndownService } from '../lib/turndown'
|
||||
import {languageMaps} from '../lib/CMLanguageList'
|
||||
import snippetManager from '../lib/SnippetManager'
|
||||
import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator'
|
||||
import markdownlint from 'markdownlint'
|
||||
import Jsonlint from 'jsonlint-mod'
|
||||
import { DEFAULT_CONFIG } from '../main/lib/ConfigManager'
|
||||
import prettier from 'prettier'
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
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')
|
||||
@@ -42,6 +54,7 @@ export default class CodeEditor extends React.Component {
|
||||
this.focusHandler = () => {
|
||||
ipcRenderer.send('editor:focused', true)
|
||||
}
|
||||
const debouncedDeletionOfAttachments = _.debounce(attachmentManagement.deleteAttachmentsNotPresentInNote, 30000)
|
||||
this.blurHandler = (editor, e) => {
|
||||
ipcRenderer.send('editor:focused', false)
|
||||
if (e == null) return null
|
||||
@@ -53,13 +66,13 @@ export default class CodeEditor extends React.Component {
|
||||
el = el.parentNode
|
||||
}
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
|
||||
const { storageKey, noteKey } = this.props
|
||||
attachmentManagement.deleteAttachmentsNotPresentInNote(
|
||||
this.editor.getValue(),
|
||||
const {
|
||||
storageKey,
|
||||
noteKey
|
||||
)
|
||||
} = this.props
|
||||
if (this.props.deleteUnusedAttachments === true) {
|
||||
debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey)
|
||||
}
|
||||
}
|
||||
this.pasteHandler = (editor, e) => {
|
||||
e.preventDefault()
|
||||
@@ -72,6 +85,8 @@ export default class CodeEditor extends React.Component {
|
||||
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||
this.searchState = null
|
||||
this.scrollToLineHandeler = this.scrollToLine.bind(this)
|
||||
this.getCodeEditorLintConfig = this.getCodeEditorLintConfig.bind(this)
|
||||
this.validatorOfMarkdown = this.validatorOfMarkdown.bind(this)
|
||||
|
||||
this.formatTable = () => this.handleFormatTable()
|
||||
|
||||
@@ -86,7 +101,7 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
this.editorActivityHandler = () => this.handleEditorActivity()
|
||||
|
||||
this.turndownService = new TurndownService()
|
||||
this.turndownService = createTurndownService()
|
||||
}
|
||||
|
||||
handleSearch (msg) {
|
||||
@@ -94,7 +109,7 @@ export default class CodeEditor extends React.Component {
|
||||
const component = this
|
||||
|
||||
if (component.searchState) cm.removeOverlay(component.searchState)
|
||||
if (msg.length < 3) return
|
||||
if (msg.length < 1) return
|
||||
|
||||
cm.operation(function () {
|
||||
component.searchState = makeOverlay(msg, 'searching')
|
||||
@@ -124,7 +139,9 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
handleFormatTable () {
|
||||
this.tableEditor.formatAll(options({textWidthOptions: {}}))
|
||||
this.tableEditor.formatAll(options({
|
||||
textWidthOptions: {}
|
||||
}))
|
||||
}
|
||||
|
||||
handleEditorActivity () {
|
||||
@@ -135,7 +152,8 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
updateDefaultKeyMap () {
|
||||
const { hotkey } = this.props
|
||||
const expandSnippet = this.expandSnippet.bind(this)
|
||||
const self = this
|
||||
const expandSnippet = snippetManager.expandSnippet
|
||||
|
||||
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
|
||||
Tab: function (cm) {
|
||||
@@ -155,14 +173,16 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
cm.execCommand('goLineEnd')
|
||||
} else if (
|
||||
!charBeforeCursor.match(/\t|\s|\r|\n/) &&
|
||||
!charBeforeCursor.match(/\t|\s|\r|\n|\$/) &&
|
||||
cursor.ch > 1
|
||||
) {
|
||||
// text expansion on tab key if the char before is alphabet
|
||||
const snippets = JSON.parse(
|
||||
fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
|
||||
const wordBeforeCursor = self.getWordBeforeCursor(
|
||||
line,
|
||||
cursor.line,
|
||||
cursor.ch
|
||||
)
|
||||
if (expandSnippet(line, cursor, cm, snippets) === false) {
|
||||
if (expandSnippet(wordBeforeCursor, cursor, cm) === false) {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
@@ -184,6 +204,14 @@ export default class CodeEditor extends React.Component {
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
[translateHotkey(hotkey.insertDate)]: function (cm) {
|
||||
const dateNow = new Date()
|
||||
cm.replaceSelection(dateNow.toLocaleDateString())
|
||||
},
|
||||
[translateHotkey(hotkey.insertDateTime)]: function (cm) {
|
||||
const dateNow = new Date()
|
||||
cm.replaceSelection(dateNow.toLocaleString())
|
||||
},
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': cm => {
|
||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
@@ -191,6 +219,37 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
return CodeMirror.Pass
|
||||
},
|
||||
[translateHotkey(hotkey.prettifyMarkdown)]: cm => {
|
||||
// Default / User configured prettier options
|
||||
const currentConfig = JSON.parse(self.props.prettierConfig)
|
||||
|
||||
// Parser type will always need to be markdown so we override the option before use
|
||||
currentConfig.parser = 'markdown'
|
||||
|
||||
// Get current cursor position
|
||||
const cursorPos = cm.getCursor()
|
||||
currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos)
|
||||
|
||||
// Prettify contents of editor
|
||||
const formattedTextDetails = prettier.formatWithCursor(cm.doc.getValue(), currentConfig)
|
||||
|
||||
const formattedText = formattedTextDetails.formatted
|
||||
const formattedCursorPos = formattedTextDetails.cursorOffset
|
||||
cm.doc.setValue(formattedText)
|
||||
|
||||
// Reset Cursor position to be at the same markdown as was before prettifying
|
||||
const newCursorPos = cm.doc.posFromIndex(formattedCursorPos)
|
||||
cm.doc.setCursor(newCursorPos)
|
||||
},
|
||||
[translateHotkey(hotkey.sortLines)]: cm => {
|
||||
const selection = cm.doc.getSelection()
|
||||
const appendLineBreak = /\n$/.test(selection)
|
||||
|
||||
const sorted = _.split(selection.trim(), '\n').sort()
|
||||
const sortedString = _.join(sorted, '\n') + (appendLineBreak ? '\n' : '')
|
||||
|
||||
cm.doc.replaceSelection(sortedString)
|
||||
},
|
||||
[translateHotkey(hotkey.pasteSmartly)]: cm => {
|
||||
this.handlePaste(cm, true)
|
||||
}
|
||||
@@ -214,25 +273,10 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { rulers, enableRulers } = this.props
|
||||
const { rulers, enableRulers, enableMarkdownLint } = 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'
|
||||
)
|
||||
}
|
||||
|
||||
snippetManager.init()
|
||||
this.updateDefaultKeyMap()
|
||||
|
||||
this.value = this.props.value
|
||||
@@ -241,7 +285,7 @@ export default class CodeEditor extends React.Component {
|
||||
value: this.props.value,
|
||||
linesHighlighted: this.props.linesHighlighted,
|
||||
lineNumbers: this.props.displayLineNumbers,
|
||||
lineWrapping: true,
|
||||
lineWrapping: this.props.lineWrapping,
|
||||
theme: this.props.theme,
|
||||
indentUnit: this.props.indentSize,
|
||||
tabSize: this.props.indentSize,
|
||||
@@ -251,17 +295,25 @@ export default class CodeEditor extends React.Component {
|
||||
inputStyle: 'textarea',
|
||||
dragDrop: false,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
autoCloseBrackets: {
|
||||
pairs: '()[]{}\'\'""$$**``',
|
||||
triples: '```"""\'\'\'',
|
||||
explode: '[]{}``$$',
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
},
|
||||
extraKeys: this.defaultKeyMap
|
||||
extraKeys: this.defaultKeyMap,
|
||||
prettierConfig: this.props.prettierConfig
|
||||
})
|
||||
|
||||
this.setMode(this.props.mode)
|
||||
document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none'
|
||||
|
||||
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)
|
||||
@@ -298,43 +350,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) {
|
||||
@@ -349,61 +475,12 @@ export default class CodeEditor extends React.Component {
|
||||
this.initialHighlighting()
|
||||
}
|
||||
|
||||
expandSnippet (line, cursor, cm, snippets) {
|
||||
const wordBeforeCursor = this.getWordBeforeCursor(
|
||||
line,
|
||||
cursor.line,
|
||||
cursor.ch
|
||||
)
|
||||
const templateCursorString = ':{}'
|
||||
for (let i = 0; i < snippets.length; i++) {
|
||||
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
|
||||
if (snippets[i].content.indexOf(templateCursorString) !== -1) {
|
||||
const snippetLines = snippets[i].content.split('\n')
|
||||
let cursorLineNumber = 0
|
||||
let cursorLinePosition = 0
|
||||
|
||||
let cursorIndex
|
||||
for (let j = 0; j < snippetLines.length; j++) {
|
||||
cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||
|
||||
if (cursorIndex !== -1) {
|
||||
cursorLineNumber = j
|
||||
cursorLinePosition = cursorIndex
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cm.replaceRange(
|
||||
snippets[i].content.replace(templateCursorString, ''),
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
cm.setCursor({
|
||||
line: cursor.line + cursorLineNumber,
|
||||
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
|
||||
})
|
||||
} else {
|
||||
cm.replaceRange(
|
||||
snippets[i].content,
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
getWordBeforeCursor (line, lineNumber, cursorPosition) {
|
||||
let wordBeforeCursor = ''
|
||||
const originCursorPosition = cursorPosition
|
||||
const emptyChars = /\t|\s|\r|\n/
|
||||
const emptyChars = /\t|\s|\r|\n|\$/
|
||||
|
||||
// to prevent the word to expand is long that will crash the whole app
|
||||
// to prevent the word is long that will crash the whole app
|
||||
// the safeStop is there to stop user to expand words that longer than 20 chars
|
||||
const safeStop = 20
|
||||
|
||||
@@ -413,7 +490,7 @@ export default class CodeEditor extends React.Component {
|
||||
if (!emptyChars.test(currentChar)) {
|
||||
wordBeforeCursor = currentChar + wordBeforeCursor
|
||||
} else if (wordBeforeCursor.length >= safeStop) {
|
||||
throw new Error('Your snippet trigger is too long !')
|
||||
throw new Error('Stopped after 20 loops for safety reason !')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -423,8 +500,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,7 +533,12 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
let needRefresh = false
|
||||
const { rulers, enableRulers } = this.props
|
||||
const {
|
||||
rulers,
|
||||
enableRulers,
|
||||
enableMarkdownLint,
|
||||
customMarkdownLintConfig
|
||||
} = this.props
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
@@ -467,6 +555,16 @@ export default class CodeEditor extends React.Component {
|
||||
if (prevProps.keyMap !== this.props.keyMap) {
|
||||
needRefresh = true
|
||||
}
|
||||
if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) {
|
||||
if (!enableMarkdownLint) {
|
||||
this.editor.setOption('lint', {default: false})
|
||||
document.querySelector('.CodeMirror-lint-markers').style.display = 'none'
|
||||
} else {
|
||||
this.editor.setOption('lint', this.getCodeEditorLintConfig())
|
||||
document.querySelector('.CodeMirror-lint-markers').style.display = 'inline-block'
|
||||
}
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.enableRulers !== enableRulers ||
|
||||
@@ -487,10 +585,26 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
|
||||
}
|
||||
|
||||
if (prevProps.lineWrapping !== this.props.lineWrapping) {
|
||||
this.editor.setOption('lineWrapping', this.props.lineWrapping)
|
||||
}
|
||||
|
||||
if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
|
||||
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)
|
||||
@@ -523,20 +637,73 @@ 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'})
|
||||
}
|
||||
}
|
||||
if (prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments) {
|
||||
this.editor.setOption('deleteUnusedAttachments', this.props.deleteUnusedAttachments)
|
||||
}
|
||||
|
||||
if (needRefresh) {
|
||||
this.editor.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
getCodeEditorLintConfig () {
|
||||
const { mode } = this.props
|
||||
const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown'
|
||||
|
||||
return checkMarkdownNoteIsOpen ? {
|
||||
getAnnotations: this.validatorOfMarkdown,
|
||||
async: true
|
||||
} : false
|
||||
}
|
||||
|
||||
validatorOfMarkdown (text, updateLinting) {
|
||||
const { customMarkdownLintConfig } = this.props
|
||||
let lintConfigJson
|
||||
try {
|
||||
Jsonlint.parse(customMarkdownLintConfig)
|
||||
lintConfigJson = JSON.parse(customMarkdownLintConfig)
|
||||
} catch (err) {
|
||||
eventEmitter.emit('APP_SETTING_ERROR')
|
||||
return
|
||||
}
|
||||
const lintOptions = {
|
||||
strings: {
|
||||
content: text
|
||||
},
|
||||
config: lintConfigJson
|
||||
}
|
||||
|
||||
return markdownlint(lintOptions, (err, result) => {
|
||||
if (!err) {
|
||||
const foundIssues = []
|
||||
const splitText = text.split('\n')
|
||||
result.content.map(item => {
|
||||
let ruleNames = ''
|
||||
item.ruleNames.map((ruleName, index) => {
|
||||
ruleNames += ruleName
|
||||
ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/'
|
||||
})
|
||||
const lineNumber = item.lineNumber - 1
|
||||
foundIssues.push({
|
||||
from: CodeMirror.Pos(lineNumber, 0),
|
||||
to: CodeMirror.Pos(lineNumber, splitText[lineNumber].length),
|
||||
message: ruleNames + item.ruleDescription,
|
||||
severity: 'warning'
|
||||
})
|
||||
})
|
||||
updateLinting(foundIssues)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setMode (mode) {
|
||||
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)
|
||||
@@ -546,6 +713,34 @@ export default class CodeEditor extends React.Component {
|
||||
handleChange (editor, changeObject) {
|
||||
spellcheck.handleChange(editor, changeObject)
|
||||
|
||||
// The current note contains an toc. We'll check for changes on headlines.
|
||||
// origin is undefined when markdownTocGenerator replace the old tod
|
||||
if (tocExistsInEditor(editor) && changeObject.origin !== undefined) {
|
||||
let requireTocUpdate
|
||||
|
||||
// Check if one of the changed lines contains a headline
|
||||
for (let line = 0; line < changeObject.text.length; line++) {
|
||||
if (this.linePossibleContainsHeadline(editor.getLine(changeObject.from.line + line))) {
|
||||
requireTocUpdate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!requireTocUpdate) {
|
||||
// Check if one of the removed lines contains a headline
|
||||
for (let line = 0; line < changeObject.removed.length; line++) {
|
||||
if (this.linePossibleContainsHeadline(changeObject.removed[line])) {
|
||||
requireTocUpdate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requireTocUpdate) {
|
||||
generateInEditor(editor)
|
||||
}
|
||||
}
|
||||
|
||||
this.updateHighlight(editor, changeObject)
|
||||
|
||||
this.value = editor.getValue()
|
||||
@@ -554,15 +749,21 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
linePossibleContainsHeadline (currentLine) {
|
||||
// We can't check if the line start with # because when some write text before
|
||||
// the # we also need to update the toc
|
||||
return currentLine.includes('# ')
|
||||
}
|
||||
|
||||
incrementLines (start, linesAdded, linesRemoved, editor) {
|
||||
let highlightedLines = editor.options.linesHighlighted
|
||||
const highlightedLines = editor.options.linesHighlighted
|
||||
|
||||
const totalHighlightedLines = highlightedLines.length
|
||||
|
||||
let offset = linesAdded - linesRemoved
|
||||
const offset = linesAdded - linesRemoved
|
||||
|
||||
// Store new items to be added as we're changing the lines
|
||||
let newLines = []
|
||||
const newLines = []
|
||||
|
||||
let i = totalHighlightedLines
|
||||
|
||||
@@ -643,6 +844,9 @@ export default class CodeEditor extends React.Component {
|
||||
ch: 1
|
||||
}
|
||||
this.editor.setCursor(cursor)
|
||||
const top = this.editor.charCoords({line: num, ch: 0}, 'local').top
|
||||
const middleHeight = this.editor.getScrollerElement().offsetHeight / 2
|
||||
this.editor.scrollTo(null, top - middleHeight - 5)
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -670,9 +874,23 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setCursor(cursor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update content of one line
|
||||
* @param {Number} lineNumber
|
||||
* @param {String} content
|
||||
*/
|
||||
setLineContent (lineNumber, content) {
|
||||
const prevContent = this.editor.getLine(lineNumber)
|
||||
const prevContentLength = prevContent ? prevContent.length : 0
|
||||
this.editor.replaceRange(content, { line: lineNumber, ch: 0 }, { line: lineNumber, ch: prevContentLength })
|
||||
}
|
||||
|
||||
handleDropImage (dropEvent) {
|
||||
dropEvent.preventDefault()
|
||||
const { storageKey, noteKey } = this.props
|
||||
const {
|
||||
storageKey,
|
||||
noteKey
|
||||
} = this.props
|
||||
attachmentManagement.handleAttachmentDrop(
|
||||
this,
|
||||
storageKey,
|
||||
@@ -685,25 +903,33 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.replaceSelection(imageMd)
|
||||
}
|
||||
|
||||
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 => {
|
||||
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||
return matcher.test(str)
|
||||
}
|
||||
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 === ')'
|
||||
}
|
||||
|
||||
@@ -747,25 +973,10 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
if (isInFencedCodeBlock(editor)) {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
} else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(editor, pastedTxt)
|
||||
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(editor, pastedTxt)
|
||||
} else if (enableSmartPaste || forceSmartPaste) {
|
||||
const image = clipboard.readImage()
|
||||
if (!image.isEmpty()) {
|
||||
attachmentManagement.handlePastNativeImage(
|
||||
this,
|
||||
storageKey,
|
||||
noteKey,
|
||||
image
|
||||
)
|
||||
} else {
|
||||
const pastedHtml = clipboard.readHTML()
|
||||
if (pastedHtml.length > 0) {
|
||||
this.handlePasteHtml(editor, pastedHtml)
|
||||
} else {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
}
|
||||
}
|
||||
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||
attachmentManagement
|
||||
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||
@@ -773,7 +984,28 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.replaceSelection(modifiedText)
|
||||
})
|
||||
} else {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,7 +1016,17 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
handlePasteUrl (editor, pastedTxt) {
|
||||
const taggedUrl = `<${pastedTxt}>`
|
||||
let taggedUrl = `<${pastedTxt}>`
|
||||
let urlToFetch = pastedTxt
|
||||
let titleMark = ''
|
||||
|
||||
if (isMarkdownTitleURL(pastedTxt)) {
|
||||
const pastedTxtSplitted = pastedTxt.split(' ')
|
||||
titleMark = `${pastedTxtSplitted[0]} `
|
||||
urlToFetch = pastedTxtSplitted[1]
|
||||
taggedUrl = `<${urlToFetch}>`
|
||||
}
|
||||
|
||||
editor.replaceSelection(taggedUrl)
|
||||
|
||||
const isImageReponse = response => {
|
||||
@@ -796,22 +1038,23 @@ export default class CodeEditor extends React.Component {
|
||||
const replaceTaggedUrl = replacement => {
|
||||
const value = editor.getValue()
|
||||
const cursor = editor.getCursor()
|
||||
const newValue = value.replace(taggedUrl, replacement)
|
||||
const newValue = value.replace(taggedUrl, titleMark + replacement)
|
||||
const newCursor = Object.assign({}, cursor, {
|
||||
ch: cursor.ch + newValue.length - value.length
|
||||
ch: cursor.ch + newValue.length - (value.length - titleMark.length)
|
||||
})
|
||||
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(newCursor)
|
||||
}
|
||||
|
||||
fetch(pastedTxt, {
|
||||
fetch(urlToFetch, {
|
||||
method: 'get'
|
||||
})
|
||||
.then(response => {
|
||||
if (isImageReponse(response)) {
|
||||
return this.mapImageResponse(response, pastedTxt)
|
||||
return this.mapImageResponse(response, urlToFetch)
|
||||
} else {
|
||||
return this.mapNormalResponse(response, pastedTxt)
|
||||
return this.mapNormalResponse(response, urlToFetch)
|
||||
}
|
||||
})
|
||||
.then(replacement => {
|
||||
@@ -899,7 +1142,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)
|
||||
}
|
||||
@@ -919,20 +1162,26 @@ 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)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -966,7 +1215,11 @@ CodeEditor.propTypes = {
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
spellCheck: PropTypes.bool
|
||||
autoDetect: PropTypes.bool,
|
||||
spellCheck: PropTypes.bool,
|
||||
enableMarkdownLint: PropTypes.bool,
|
||||
customMarkdownLintConfig: PropTypes.string,
|
||||
deleteUnusedAttachments: PropTypes.bool
|
||||
}
|
||||
|
||||
CodeEditor.defaultProps = {
|
||||
@@ -977,5 +1230,10 @@ CodeEditor.defaultProps = {
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space',
|
||||
spellCheck: false
|
||||
autoDetect: false,
|
||||
spellCheck: false,
|
||||
enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint,
|
||||
customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig,
|
||||
prettierConfig: DEFAULT_CONFIG.editor.prettierConfig,
|
||||
deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments
|
||||
}
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
|
||||
.spellcheck-select
|
||||
border: none
|
||||
text-decoration underline wavy red
|
||||
|
||||
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%)
|
||||
|
||||
|
||||
@@ -20,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()
|
||||
@@ -32,6 +32,7 @@ class MarkdownEditor extends React.Component {
|
||||
componentDidMount () {
|
||||
this.value = this.refs.code.value
|
||||
eventEmitter.on('editor:lock', this.lockEditorCode)
|
||||
eventEmitter.on('editor:focus', this.focusEditor.bind(this))
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
@@ -47,6 +48,15 @@ class MarkdownEditor extends React.Component {
|
||||
componentWillUnmount () {
|
||||
this.cancelQueue()
|
||||
eventEmitter.off('editor:lock', this.lockEditorCode)
|
||||
eventEmitter.off('editor:focus', this.focusEditor.bind(this))
|
||||
}
|
||||
|
||||
focusEditor () {
|
||||
this.setState({
|
||||
status: 'CODE'
|
||||
}, () => {
|
||||
this.refs.code.focus()
|
||||
})
|
||||
}
|
||||
|
||||
queueRendering (value) {
|
||||
@@ -76,6 +86,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'
|
||||
@@ -108,7 +119,7 @@ class MarkdownEditor extends React.Component {
|
||||
status: 'PREVIEW'
|
||||
}, () => {
|
||||
this.refs.preview.focus()
|
||||
this.refs.preview.scrollTo(cursorPosition.line)
|
||||
this.refs.preview.scrollToRow(cursorPosition.line)
|
||||
})
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
}
|
||||
@@ -148,24 +159,25 @@ class MarkdownEditor extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
const checkReplace = /\[x\]/i
|
||||
const uncheckReplace = /\[ \]/
|
||||
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
|
||||
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
|
||||
const checkReplace = /\[x]/i
|
||||
const uncheckReplace = /\[ ]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
.split('\n')
|
||||
|
||||
const targetLine = lines[lineIndex]
|
||||
let newLine = targetLine
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
||||
newLine = targetLine.replace(checkReplace, '[ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
||||
newLine = targetLine.replace(uncheckReplace, '[x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
this.refs.code.setLineContent(lineIndex, newLine)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +305,10 @@ class MarkdownEditor extends React.Component {
|
||||
enableRulers={config.editor.enableRulers}
|
||||
rulers={config.editor.rulers}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
lineWrapping
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
@@ -305,6 +321,10 @@ class MarkdownEditor extends React.Component {
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
switchPreview={config.editor.switchPreview}
|
||||
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||
prettierConfig={config.editor.prettierConfig}
|
||||
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||
/>
|
||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||
? 'preview'
|
||||
@@ -324,6 +344,7 @@ class MarkdownEditor extends React.Component {
|
||||
smartArrows={config.preview.smartArrows}
|
||||
breaks={config.preview.breaks}
|
||||
sanitize={config.preview.sanitize}
|
||||
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
|
||||
ref='preview'
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||
|
||||
@@ -8,7 +8,7 @@ import consts from 'browser/lib/consts'
|
||||
import Raphael from 'raphael'
|
||||
import flowchart from 'flowchart'
|
||||
import mermaidRender from './render/MermaidRender'
|
||||
import SequenceDiagram from 'js-sequence-diagrams'
|
||||
import SequenceDiagram from '@rokt33r/js-sequence-diagrams'
|
||||
import Chart from 'chart.js'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||
@@ -18,15 +18,13 @@ import mdurl from 'mdurl'
|
||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||
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')
|
||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu
|
||||
|
||||
const { app } = remote
|
||||
const path = require('path')
|
||||
@@ -34,8 +32,6 @@ const fileUrl = require('file-url')
|
||||
|
||||
const dialog = remote.dialog
|
||||
|
||||
const uri2path = require('file-uri-to-path')
|
||||
|
||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||
const appPath = fileUrl(
|
||||
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
||||
@@ -46,16 +42,29 @@ const CSS_FILES = [
|
||||
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
|
||||
]
|
||||
|
||||
function buildStyle (
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
) {
|
||||
/**
|
||||
* @param {Object} opts
|
||||
* @param {String} opts.fontFamily
|
||||
* @param {Numberl} opts.fontSize
|
||||
* @param {String} opts.codeBlockFontFamily
|
||||
* @param {String} opts.theme
|
||||
* @param {Boolean} [opts.lineNumber] Should show line number
|
||||
* @param {Boolean} [opts.scrollPastEnd]
|
||||
* @param {Boolean} [opts.allowCustomCSS] Should add custom css
|
||||
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
|
||||
* @returns {String}
|
||||
*/
|
||||
function buildStyle (opts) {
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = opts
|
||||
return `
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
@@ -85,12 +94,17 @@ function buildStyle (
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
${markdownStyle}
|
||||
|
||||
body {
|
||||
font-family: '${fontFamily.join("','")}';
|
||||
font-size: ${fontSize}px;
|
||||
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
||||
${scrollPastEnd ? `
|
||||
padding-bottom: 90vh;
|
||||
box-sizing: border-box;
|
||||
`
|
||||
: ''}
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
@@ -166,6 +180,10 @@ const scrollBarStyle = `
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: inherit;
|
||||
}
|
||||
`
|
||||
const scrollBarDarkStyle = `
|
||||
::-webkit-scrollbar {
|
||||
@@ -175,6 +193,10 @@ const scrollBarDarkStyle = `
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: inherit;
|
||||
}
|
||||
`
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
@@ -192,6 +214,19 @@ const defaultCodeBlockFontFamily = [
|
||||
'source-code-pro',
|
||||
'monospace'
|
||||
]
|
||||
|
||||
// return the line number of the line that used to generate the specified element
|
||||
// return -1 if the line is not found
|
||||
function getSourceLineNumberByElement (element) {
|
||||
let isHasLineNumber = element.dataset.line !== undefined
|
||||
let parent = element
|
||||
while (!isHasLineNumber && parent.parentElement !== null) {
|
||||
parent = parent.parentElement
|
||||
isHasLineNumber = parent.dataset.line !== undefined
|
||||
}
|
||||
return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1
|
||||
}
|
||||
|
||||
export default class MarkdownPreview extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -208,6 +243,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||
this.saveAsPdfHandler = () => this.handleSaveAsPdf()
|
||||
this.printHandler = () => this.handlePrint()
|
||||
this.resizeHandler = _.throttle(this.handleResize.bind(this), 100)
|
||||
|
||||
@@ -236,30 +272,12 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleContextMenu (event) {
|
||||
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
|
||||
if (_.isFunction(this.props.onContextMenu)) {
|
||||
const menu = buildMarkdownPreviewContextMenu(this, event)
|
||||
const switchPreview = ConfigManager.get().editor.switchPreview
|
||||
if (menu != null && switchPreview !== 'RIGHTCLICK') {
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
} else if (_.isFunction(this.props.onContextMenu)) {
|
||||
this.props.onContextMenu(event)
|
||||
return
|
||||
}
|
||||
// No contextMenu was passed to us -> execute our own link-opener
|
||||
if (event.target.tagName.toLowerCase() === 'a') {
|
||||
const href = event.target.href
|
||||
const isLocalFile = href.startsWith('file:')
|
||||
if (isLocalFile) {
|
||||
const absPath = uri2path(href)
|
||||
try {
|
||||
if (fs.lstatSync(absPath).isFile()) {
|
||||
context.popup([
|
||||
{
|
||||
label: i18n.__('Show in explorer'),
|
||||
click: (e) => shell.showItemInFolder(absPath)
|
||||
}
|
||||
])
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error while evaluating if the file is locally available', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,17 +287,27 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
handleMouseDown (e) {
|
||||
const config = ConfigManager.get()
|
||||
const clickElement = e.target
|
||||
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV"
|
||||
const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
|
||||
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
||||
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
||||
}
|
||||
if (e.target != null) {
|
||||
switch (e.target.tagName) {
|
||||
case 'A':
|
||||
case 'INPUT':
|
||||
return null
|
||||
if (e.ctrlKey) {
|
||||
if (config.editor.type === 'SPLIT') {
|
||||
if (lineNumber !== -1) {
|
||||
eventEmitter.emit('line:jump', lineNumber)
|
||||
}
|
||||
} else {
|
||||
if (lineNumber !== -1) {
|
||||
eventEmitter.emit('editor:focus')
|
||||
eventEmitter.emit('line:jump', lineNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
|
||||
|
||||
if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e)
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
@@ -298,58 +326,81 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.exportAsDocument('md')
|
||||
}
|
||||
|
||||
handleSaveAsHtml () {
|
||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.getStyleParams()
|
||||
htmlContentFormatter (noteContent, exportTasks, targetDir) {
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
)
|
||||
let body = this.markdown.render(noteContent)
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
files.forEach(file => {
|
||||
if (global.process.platform === 'win32') {
|
||||
file = file.replace('file:///', '')
|
||||
} else {
|
||||
file = file.replace('file://', '')
|
||||
}
|
||||
exportTasks.push({
|
||||
src: file,
|
||||
dst: 'css'
|
||||
const inlineStyles = buildStyle({
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
})
|
||||
let body = this.markdown.render(noteContent)
|
||||
body = attachmentManagement.fixLocalURLS(
|
||||
body,
|
||||
this.props.storagePath
|
||||
)
|
||||
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
files.forEach(file => {
|
||||
if (global.process.platform === 'win32') {
|
||||
file = file.replace('file:///', '')
|
||||
} else {
|
||||
file = file.replace('file://', '')
|
||||
}
|
||||
exportTasks.push({
|
||||
src: file,
|
||||
dst: 'css'
|
||||
})
|
||||
})
|
||||
|
||||
let styles = ''
|
||||
files.forEach(file => {
|
||||
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||
})
|
||||
|
||||
return `<html>
|
||||
<head>
|
||||
<base href="file://${targetDir}/">
|
||||
<meta charset="UTF-8">
|
||||
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||
<style id="style">${inlineStyles}</style>
|
||||
${styles}
|
||||
</head>
|
||||
<body>${body}</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
handleSaveAsHtml () {
|
||||
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir)))
|
||||
}
|
||||
|
||||
handleSaveAsPdf () {
|
||||
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
|
||||
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false, javascript: false}})
|
||||
printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir))
|
||||
return new Promise((resolve, reject) => {
|
||||
printout.webContents.on('did-finish-load', () => {
|
||||
printout.webContents.printToPDF({}, (err, data) => {
|
||||
if (err) reject(err)
|
||||
else resolve(data)
|
||||
printout.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
let styles = ''
|
||||
files.forEach(file => {
|
||||
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||
})
|
||||
|
||||
return `<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||
<style id="style">${inlineStyles}</style>
|
||||
${styles}
|
||||
</head>
|
||||
<body>${body}</body>
|
||||
</html>`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -399,6 +450,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
|
||||
|
||||
@@ -470,6 +546,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||
eventEmitter.on('export:save-pdf', this.saveAsPdfHandler)
|
||||
eventEmitter.on('print', this.printHandler)
|
||||
}
|
||||
|
||||
@@ -511,20 +588,24 @@ export default class MarkdownPreview extends React.Component {
|
||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||
eventEmitter.off('export:save-pdf', this.saveAsPdfHandler)
|
||||
eventEmitter.off('print', this.printHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||
// actual rewriteIframe function should be called only once
|
||||
let needsRewriteIframe = false
|
||||
if (prevProps.value !== this.props.value) needsRewriteIframe = true
|
||||
if (
|
||||
prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||
prevProps.sanitize !== this.props.sanitize ||
|
||||
prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel ||
|
||||
prevProps.smartArrows !== this.props.smartArrows ||
|
||||
prevProps.breaks !== this.props.breaks ||
|
||||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
|
||||
) {
|
||||
this.initMarkdown()
|
||||
this.rewriteIframe()
|
||||
needsRewriteIframe = true
|
||||
}
|
||||
if (
|
||||
prevProps.fontFamily !== this.props.fontFamily ||
|
||||
@@ -539,8 +620,17 @@ export default class MarkdownPreview extends React.Component {
|
||||
prevProps.customCSS !== this.props.customCSS
|
||||
) {
|
||||
this.applyStyle()
|
||||
needsRewriteIframe = true
|
||||
}
|
||||
|
||||
if (needsRewriteIframe) {
|
||||
this.rewriteIframe()
|
||||
}
|
||||
|
||||
// Should scroll to top after selecting another note
|
||||
if (prevProps.noteKey !== this.props.noteKey) {
|
||||
this.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
getStyleParams () {
|
||||
@@ -596,8 +686,8 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
this.getWindow().document.getElementById(
|
||||
'codeTheme'
|
||||
).href = this.GetCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
||||
).href = this.getCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle({
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
@@ -606,17 +696,15 @@ export default class MarkdownPreview extends React.Component {
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
GetCodeThemeLink (theme) {
|
||||
theme = consts.THEMES.some(_theme => _theme === theme) &&
|
||||
theme !== 'default'
|
||||
? theme
|
||||
: 'elegant'
|
||||
return theme.startsWith('solarized')
|
||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
||||
getCodeThemeLink (name) {
|
||||
const theme = consts.THEMES.find(theme => theme.name === name)
|
||||
|
||||
return theme != null
|
||||
? theme.path
|
||||
: `${appPath}/node_modules/codemirror/theme/elegant.css`
|
||||
}
|
||||
|
||||
rewriteIframe () {
|
||||
@@ -641,11 +729,17 @@ export default class MarkdownPreview extends React.Component {
|
||||
indentSize,
|
||||
showCopyNotification,
|
||||
storagePath,
|
||||
noteKey
|
||||
noteKey,
|
||||
sanitize,
|
||||
mermaidHTMLLabel
|
||||
} = 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(
|
||||
@@ -669,9 +763,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
)
|
||||
|
||||
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
|
||||
? codeBlockTheme
|
||||
: 'default'
|
||||
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
|
||||
|
||||
const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default'
|
||||
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
||||
@@ -684,6 +778,8 @@ export default class MarkdownPreview extends React.Component {
|
||||
copyIcon.innerHTML =
|
||||
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||
copyIcon.onclick = e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
copy(content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
@@ -692,14 +788,11 @@ export default class MarkdownPreview extends React.Component {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
el.parentNode.appendChild(copyIcon)
|
||||
el.innerHTML = ''
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||
} else {
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||
}
|
||||
el.parentNode.className += ` ${codeBlockThemeClassName}`
|
||||
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: indentSize
|
||||
})
|
||||
@@ -770,6 +863,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
canvas.height = height.value + 'vh'
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const chart = new Chart(canvas, chartConfig)
|
||||
} catch (e) {
|
||||
el.className = 'chart-error'
|
||||
@@ -780,7 +874,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
||||
el => {
|
||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
|
||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme, mermaidHTMLLabel)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -811,6 +905,103 @@ export default class MarkdownPreview extends React.Component {
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
|
||||
const rect = markdownPreviewIframe.getBoundingClientRect()
|
||||
const config = { attributes: true, subtree: true }
|
||||
const imgObserver = new MutationObserver((mutationList) => {
|
||||
for (const mu of mutationList) {
|
||||
if (mu.target.className === 'carouselContent-enter-done') {
|
||||
this.setImgOnClickEventHelper(mu.target, rect)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
|
||||
for (const img of imgList) {
|
||||
const parentEl = img.parentElement
|
||||
this.setImgOnClickEventHelper(img, rect)
|
||||
imgObserver.observe(parentEl, config)
|
||||
}
|
||||
|
||||
const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('a')
|
||||
for (const a of aList) {
|
||||
a.removeEventListener('click', this.linkClickHandler)
|
||||
a.addEventListener('click', this.linkClickHandler)
|
||||
}
|
||||
}
|
||||
|
||||
setImgOnClickEventHelper (img, rect) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
handleResize () {
|
||||
@@ -830,7 +1021,11 @@ export default class MarkdownPreview extends React.Component {
|
||||
return this.refs.root.contentWindow
|
||||
}
|
||||
|
||||
scrollTo (targetRow) {
|
||||
/**
|
||||
* @public
|
||||
* @param {Number} targetRow
|
||||
*/
|
||||
scrollToRow (targetRow) {
|
||||
const blocks = this.getWindow().document.querySelectorAll(
|
||||
'body>[data-line]'
|
||||
)
|
||||
@@ -840,12 +1035,21 @@ export default class MarkdownPreview extends React.Component {
|
||||
const row = parseInt(block.getAttribute('data-line'))
|
||||
if (row > targetRow || index === blocks.length - 1) {
|
||||
block = blocks[index - 1]
|
||||
block != null && this.getWindow().scrollTo(0, block.offsetTop)
|
||||
block != null && this.scrollTo(0, block.offsetTop)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `document.body.scrollTo`
|
||||
* @param {Number} x
|
||||
* @param {Number} y
|
||||
*/
|
||||
scrollTo (x, y) {
|
||||
this.getWindow().document.body.scrollTo(x, y)
|
||||
}
|
||||
|
||||
preventImageDroppedHandler (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@@ -866,20 +1070,32 @@ export default class MarkdownPreview extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const href = e.target.href
|
||||
const linkHash = href.split('/').pop()
|
||||
const rawHref = e.target.getAttribute('href')
|
||||
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
|
||||
|
||||
const regexNoteInternalLink = /main.html#(.+)/
|
||||
if (regexNoteInternalLink.test(linkHash)) {
|
||||
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
|
||||
const targetElement = this.refs.root.contentWindow.document.getElementById(
|
||||
targetId
|
||||
)
|
||||
const parser = document.createElement('a')
|
||||
parser.href = rawHref
|
||||
const isStartWithHash = rawHref[0] === '#'
|
||||
const { href, hash } = parser
|
||||
|
||||
if (targetElement != null) {
|
||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
|
||||
|
||||
const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html
|
||||
const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`)
|
||||
if (isStartWithHash || regexNoteInternalLink.test(rawHref)) {
|
||||
const posOfHash = linkHash.indexOf('#')
|
||||
if (posOfHash > -1) {
|
||||
const extractedId = linkHash.slice(posOfHash + 1)
|
||||
const targetId = mdurl.encode(extractedId)
|
||||
const targetElement = this.getWindow().document.getElementById(
|
||||
targetId
|
||||
)
|
||||
|
||||
if (targetElement != null) {
|
||||
this.scrollTo(0, targetElement.offsetTop)
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// this will match the new uuid v4 hash and the old hash
|
||||
|
||||
@@ -78,24 +78,25 @@ class MarkdownSplitEditor extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
const checkReplace = /\[x\]/i
|
||||
const uncheckReplace = /\[ \]/
|
||||
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
|
||||
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
|
||||
const checkReplace = /\[x]/i
|
||||
const uncheckReplace = /\[ ]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
.split('\n')
|
||||
|
||||
const targetLine = lines[lineIndex]
|
||||
let newLine = targetLine
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
||||
newLine = targetLine.replace(checkReplace, '[ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
||||
newLine = targetLine.replace(uncheckReplace, '[x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
this.refs.code.setLineContent(lineIndex, newLine)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +151,6 @@ class MarkdownSplitEditor extends React.Component {
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}>
|
||||
<CodeEditor
|
||||
styleName='codeEditor'
|
||||
ref='code'
|
||||
width={this.state.codeEditorWidthInPercent + '%'}
|
||||
mode='Boost Flavored Markdown'
|
||||
@@ -160,6 +160,10 @@ class MarkdownSplitEditor extends React.Component {
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
lineWrapping
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
enableRulers={config.editor.enableRulers}
|
||||
@@ -176,13 +180,15 @@ class MarkdownSplitEditor extends React.Component {
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
switchPreview={config.editor.switchPreview}
|
||||
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||
/>
|
||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
<MarkdownPreview
|
||||
style={previewStyle}
|
||||
styleName='preview'
|
||||
theme={config.ui.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
fontSize={config.preview.fontSize}
|
||||
@@ -195,6 +201,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
smartArrows={config.preview.smartArrows}
|
||||
breaks={config.preview.breaks}
|
||||
sanitize={config.preview.sanitize}
|
||||
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
|
||||
ref='preview'
|
||||
tabInde='0'
|
||||
value={value}
|
||||
|
||||
@@ -8,9 +8,30 @@
|
||||
top -2px
|
||||
width 0
|
||||
z-index 0
|
||||
border-left 1px solid $ui-borderColor
|
||||
.slider-hitbox
|
||||
absolute top bottom left right
|
||||
width 7px
|
||||
left -3px
|
||||
z-index 10
|
||||
cursor col-resize
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
.slider
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
.slider
|
||||
border-left 1px solid $ui-solarized-dark-borderColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
.slider
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
.slider
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
|
||||
@@ -8,7 +8,7 @@ const ModalEscButton = ({
|
||||
}) => (
|
||||
<button styleName='escButton' onClick={handleEscButtonClick}>
|
||||
<div styleName='esc-mark'>×</div>
|
||||
<div styleName='esc-text'>esc</div>
|
||||
<div>esc</div>
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ const NavToggleButton = ({isFolded, handleToggleButtonClick}) => (
|
||||
onClick={(e) => handleToggleButtonClick(e)}
|
||||
>
|
||||
{isFolded
|
||||
? <i className='fa fa-angle-double-right' />
|
||||
: <i className='fa fa-angle-double-left' />
|
||||
? <i className='fa fa-angle-double-right fa-2x' />
|
||||
: <i className='fa fa-angle-double-left fa-2x' />
|
||||
}
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
border-radius 16.5px
|
||||
height 34px
|
||||
width 34px
|
||||
line-height 32px
|
||||
line-height 100%
|
||||
padding 0
|
||||
&:hover
|
||||
border: 1px solid #1EC38B;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
*/
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { isArray } from 'lodash'
|
||||
import { isArray, sortBy } 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,
|
||||
@@ -135,15 +148,14 @@ NoteItem.propTypes = {
|
||||
tags: PropTypes.array,
|
||||
isStarred: PropTypes.bool.isRequired,
|
||||
isTrashed: PropTypes.bool.isRequired,
|
||||
blog: {
|
||||
blog: PropTypes.shape({
|
||||
blogLink: PropTypes.string,
|
||||
blogId: PropTypes.number
|
||||
}
|
||||
})
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||
handleDragStart: PropTypes.func.isRequired,
|
||||
handleDragEnd: PropTypes.func.isRequired
|
||||
handleDragStart: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(NoteItem, styles)
|
||||
|
||||
@@ -74,7 +74,7 @@ SideNavFilter.propTypes = {
|
||||
isStarredActive: PropTypes.bool.isRequired,
|
||||
isTrashedActive: PropTypes.bool.isRequired,
|
||||
handleStarredButtonClick: PropTypes.func.isRequired,
|
||||
handleTrashdButtonClick: PropTypes.func.isRequired
|
||||
handleTrashedButtonClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(SideNavFilter, styles)
|
||||
|
||||
@@ -114,7 +114,7 @@ class SnippetTab extends React.Component {
|
||||
>
|
||||
{snippet.name.trim().length > 0
|
||||
? snippet.name
|
||||
: <span styleName='button-unnamed'>
|
||||
: <span>
|
||||
{i18n.__('Unnamed')}
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,10 +25,10 @@ const TodoProcess = ({
|
||||
)
|
||||
|
||||
TodoProcess.propTypes = {
|
||||
todoStatus: {
|
||||
todoStatus: PropTypes.exact({
|
||||
total: PropTypes.number.isRequired,
|
||||
completed: PropTypes.number.isRequired
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(TodoProcess, styles)
|
||||
|
||||
@@ -61,6 +61,8 @@ body
|
||||
// allow inline line breaks
|
||||
.katex
|
||||
white-space initial
|
||||
.katex .katex-html
|
||||
display inline-flex
|
||||
.katex .mfrac>.vlist>span:nth-child(2)
|
||||
top 0 !important
|
||||
.katex-error
|
||||
@@ -163,6 +165,7 @@ p
|
||||
white-space pre-line
|
||||
word-wrap break-word
|
||||
img
|
||||
cursor zoom-in
|
||||
max-width 100%
|
||||
strong, b
|
||||
font-weight bold
|
||||
@@ -360,7 +363,10 @@ admonition_types = {
|
||||
danger: {color: #c2185b, icon: "block"},
|
||||
caution: {color: #ffa726, icon: "warning"},
|
||||
error: {color: #d32f2f, icon: "error_outline"},
|
||||
attention: {color: #455a64, icon: "priority_high"}
|
||||
question: {color: #64dd17, icon: "help_outline"},
|
||||
quote: {color: #9e9e9e, icon: "format_quote"},
|
||||
abstract: {color: #00b0ff, icon: "subject"},
|
||||
attention: {color: #455a64, icon: "priority_high"},
|
||||
}
|
||||
|
||||
for name, val in admonition_types
|
||||
|
||||
@@ -19,7 +19,7 @@ function getId () {
|
||||
return id
|
||||
}
|
||||
|
||||
function render (element, content, theme) {
|
||||
function render (element, content, theme, enableHTMLLabel) {
|
||||
try {
|
||||
const height = element.attributes.getNamedItem('data-height')
|
||||
const isPredefined = height && height.value !== 'undefined'
|
||||
@@ -30,6 +30,9 @@ function render (element, content, theme) {
|
||||
mermaidAPI.initialize({
|
||||
theme: isDarkTheme ? 'dark' : 'default',
|
||||
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
||||
flowchart: {
|
||||
htmlLabels: enableHTMLLabel
|
||||
},
|
||||
gantt: {
|
||||
useWidth: element.clientWidth
|
||||
}
|
||||
@@ -51,6 +54,7 @@ function render (element, content, theme) {
|
||||
|
||||
el.setAttribute('ratio', ratio)
|
||||
el.setAttribute('height', el.parentNode.clientWidth / ratio)
|
||||
console.log(el)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user