mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 09:46:22 +00:00
User want to click markdown to work with text (for example copy). If they have "Switch to Preview" setting in "When Editor Blurred", they enter edit mode after click. To fix that issue I introduced new value to the "Switch to Preview" setting that do that thing. User can enter edit mode by double click on the editor and leave it on blur. Unfortunately, it's impossible to switch both ways by double click because the editor mode is listening for double click event. #1523
305 lines
8.9 KiB
JavaScript
305 lines
8.9 KiB
JavaScript
import PropTypes from 'prop-types'
|
|
import React from 'react'
|
|
import CSSModules from 'browser/lib/CSSModules'
|
|
import styles from './MarkdownEditor.styl'
|
|
import CodeEditor from 'browser/components/CodeEditor'
|
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
|
import {findStorage} from 'browser/lib/findStorage'
|
|
|
|
class MarkdownEditor extends React.Component {
|
|
constructor (props) {
|
|
super(props)
|
|
|
|
// char codes for ctrl + w
|
|
this.escapeFromEditor = [17, 87]
|
|
|
|
// ctrl + shift + ;
|
|
this.supportMdSelectionBold = [16, 17, 186]
|
|
|
|
this.state = {
|
|
status: 'PREVIEW',
|
|
renderValue: props.value,
|
|
keyPressed: new Set(),
|
|
isLocked: false
|
|
}
|
|
|
|
this.lockEditorCode = () => this.handleLockEditor()
|
|
}
|
|
|
|
componentDidMount () {
|
|
this.value = this.refs.code.value
|
|
eventEmitter.on('editor:lock', this.lockEditorCode)
|
|
}
|
|
|
|
componentDidUpdate () {
|
|
this.value = this.refs.code.value
|
|
}
|
|
|
|
componentWillReceiveProps (props) {
|
|
if (props.value !== this.props.value) {
|
|
this.queueRendering(props.value)
|
|
}
|
|
}
|
|
|
|
componentWillUnmount () {
|
|
this.cancelQueue()
|
|
eventEmitter.off('editor:lock', this.lockEditorCode)
|
|
}
|
|
|
|
queueRendering (value) {
|
|
clearTimeout(this.renderTimer)
|
|
this.renderTimer = setTimeout(() => {
|
|
this.renderPreview(value)
|
|
}, 500)
|
|
}
|
|
|
|
cancelQueue () {
|
|
clearTimeout(this.renderTimer)
|
|
}
|
|
|
|
renderPreview (value) {
|
|
this.setState({
|
|
renderValue: value
|
|
})
|
|
}
|
|
|
|
handleChange (e) {
|
|
this.value = this.refs.code.value
|
|
this.props.onChange(e)
|
|
}
|
|
|
|
handleContextMenu (e) {
|
|
const { config } = this.props
|
|
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
|
const newStatus = this.state.status === 'PREVIEW'
|
|
? 'CODE'
|
|
: 'PREVIEW'
|
|
this.setState({
|
|
status: newStatus
|
|
}, () => {
|
|
if (newStatus === 'CODE') {
|
|
this.refs.code.focus()
|
|
} else {
|
|
this.refs.preview.focus()
|
|
}
|
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
|
})
|
|
}
|
|
}
|
|
|
|
handleBlur (e) {
|
|
if (this.state.isLocked) return
|
|
this.setState({ keyPressed: new Set() })
|
|
const { config } = this.props
|
|
if (config.editor.switchPreview === 'BLUR' ||
|
|
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
|
|
) {
|
|
const cursorPosition = this.refs.code.editor.getCursor()
|
|
this.setState({
|
|
status: 'PREVIEW'
|
|
}, () => {
|
|
this.refs.preview.focus()
|
|
this.refs.preview.scrollTo(cursorPosition.line)
|
|
})
|
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
|
}
|
|
}
|
|
|
|
handleDoubleClick (e) {
|
|
if (this.state.isLocked) return
|
|
this.setState({keyPressed: new Set()})
|
|
const { config } = this.props
|
|
if (config.editor.switchPreview === 'DBL_CLICK') {
|
|
this.setState({
|
|
status: 'CODE'
|
|
}, () => {
|
|
this.refs.code.focus()
|
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
|
})
|
|
}
|
|
}
|
|
|
|
handlePreviewMouseDown (e) {
|
|
this.previewMouseDownedAt = new Date()
|
|
}
|
|
|
|
handlePreviewMouseUp (e) {
|
|
const { config } = this.props
|
|
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
|
|
this.setState({
|
|
status: 'CODE'
|
|
}, () => {
|
|
this.refs.code.focus()
|
|
})
|
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
|
}
|
|
}
|
|
|
|
handleCheckboxClick (e) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
const idMatch = /checkbox-([0-9]+)/
|
|
const checkedMatch = /\[x\]/i
|
|
const uncheckedMatch = /\[ \]/
|
|
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]
|
|
|
|
if (targetLine.match(checkedMatch)) {
|
|
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
|
}
|
|
if (targetLine.match(uncheckedMatch)) {
|
|
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
|
}
|
|
this.refs.code.setValue(lines.join('\n'))
|
|
}
|
|
}
|
|
|
|
focus () {
|
|
if (this.state.status === 'PREVIEW') {
|
|
this.setState({
|
|
status: 'CODE'
|
|
}, () => {
|
|
this.refs.code.focus()
|
|
})
|
|
} else {
|
|
this.refs.code.focus()
|
|
}
|
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
|
}
|
|
|
|
reload () {
|
|
this.refs.code.reload()
|
|
this.cancelQueue()
|
|
this.renderPreview(this.props.value)
|
|
}
|
|
|
|
handleKeyDown (e) {
|
|
const { config } = this.props
|
|
if (this.state.status !== 'CODE') return false
|
|
const keyPressed = this.state.keyPressed
|
|
keyPressed.add(e.keyCode)
|
|
this.setState({ keyPressed })
|
|
const isNoteHandlerKey = (el) => { return keyPressed.has(el) }
|
|
// These conditions are for ctrl-e and ctrl-w
|
|
if (keyPressed.size === this.escapeFromEditor.length &&
|
|
!this.state.isLocked && this.state.status === 'CODE' &&
|
|
this.escapeFromEditor.every(isNoteHandlerKey)) {
|
|
this.handleContextMenu()
|
|
if (config.editor.switchPreview === 'BLUR') document.activeElement.blur()
|
|
}
|
|
if (keyPressed.size === this.supportMdSelectionBold.length && this.supportMdSelectionBold.every(isNoteHandlerKey)) {
|
|
this.addMdAroundWord('**')
|
|
}
|
|
}
|
|
|
|
addMdAroundWord (mdElement) {
|
|
if (this.refs.code.editor.getSelection()) {
|
|
return this.addMdAroundSelection(mdElement)
|
|
}
|
|
const currentCaret = this.refs.code.editor.getCursor()
|
|
const word = this.refs.code.editor.findWordAt(currentCaret)
|
|
const cmDoc = this.refs.code.editor.getDoc()
|
|
cmDoc.replaceRange(mdElement, word.anchor)
|
|
cmDoc.replaceRange(mdElement, { line: word.head.line, ch: word.head.ch + mdElement.length })
|
|
}
|
|
|
|
addMdAroundSelection (mdElement) {
|
|
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
|
|
}
|
|
|
|
handleKeyUp (e) {
|
|
const keyPressed = this.state.keyPressed
|
|
keyPressed.delete(e.keyCode)
|
|
this.setState({ keyPressed })
|
|
}
|
|
|
|
handleLockEditor () {
|
|
this.setState({ isLocked: !this.state.isLocked })
|
|
}
|
|
|
|
render () {
|
|
const { className, value, config, storageKey } = this.props
|
|
|
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
|
|
|
const previewStyle = {}
|
|
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
|
|
|
const storage = findStorage(storageKey)
|
|
|
|
return (
|
|
<div className={className == null
|
|
? 'MarkdownEditor'
|
|
: `MarkdownEditor ${className}`
|
|
}
|
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
|
tabIndex='-1'
|
|
onKeyDown={(e) => this.handleKeyDown(e)}
|
|
onKeyUp={(e) => this.handleKeyUp(e)}
|
|
>
|
|
<CodeEditor styleName={this.state.status === 'CODE'
|
|
? 'codeEditor'
|
|
: 'codeEditor--hide'
|
|
}
|
|
ref='code'
|
|
mode='GitHub Flavored Markdown'
|
|
value={value}
|
|
theme={config.editor.theme}
|
|
keyMap={config.editor.keyMap}
|
|
fontFamily={config.editor.fontFamily}
|
|
fontSize={editorFontSize}
|
|
indentType={config.editor.indentType}
|
|
indentSize={editorIndentSize}
|
|
displayLineNumbers={config.editor.displayLineNumbers}
|
|
scrollPastEnd={config.editor.scrollPastEnd}
|
|
storageKey={storageKey}
|
|
onChange={(e) => this.handleChange(e)}
|
|
onBlur={(e) => this.handleBlur(e)}
|
|
/>
|
|
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
|
? 'preview'
|
|
: 'preview--hide'
|
|
}
|
|
style={previewStyle}
|
|
theme={config.ui.theme}
|
|
keyMap={config.editor.keyMap}
|
|
fontSize={config.preview.fontSize}
|
|
fontFamily={config.preview.fontFamily}
|
|
codeBlockTheme={config.preview.codeBlockTheme}
|
|
codeBlockFontFamily={config.editor.fontFamily}
|
|
lineNumber={config.preview.lineNumber}
|
|
indentSize={editorIndentSize}
|
|
scrollPastEnd={config.preview.scrollPastEnd}
|
|
ref='preview'
|
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
|
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
|
tabIndex='0'
|
|
value={this.state.renderValue}
|
|
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
|
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
|
|
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
|
showCopyNotification={config.ui.showCopyNotification}
|
|
storagePath={storage.path}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
MarkdownEditor.propTypes = {
|
|
className: PropTypes.string,
|
|
value: PropTypes.string,
|
|
onChange: PropTypes.func,
|
|
ignorePreviewPointerEvents: PropTypes.bool
|
|
}
|
|
|
|
export default CSSModules(MarkdownEditor, styles)
|