import React from 'react' import CodeEditor from 'browser/components/CodeEditor' import MarkdownPreview from 'browser/components/MarkdownPreview' import { findStorage } from 'browser/lib/findStorage' import _ from 'lodash' import styles from './MarkdownSplitEditor.styl' import CSSModules from 'browser/lib/CSSModules' class MarkdownSplitEditor extends React.Component { constructor(props) { super(props) this.value = props.value this.focus = () => this.refs.code.focus() this.reload = () => this.refs.code.reload() this.userScroll = props.config.preview.scrollSync this.state = { isSliderFocused: false, codeEditorWidthInPercent: 50, codeEditorHeightInPercent: 50 } } componentDidUpdate(prevProps) { if ( this.props.config.preview.scrollSync !== prevProps.config.preview.scrollSync ) { this.userScroll = this.props.config.preview.scrollSync } } handleCursorActivity(editor) { if (this.userScroll) { const previewDoc = _.get( this, 'refs.preview.refs.root.contentWindow.document' ) const previewTop = _.get(previewDoc, 'body.scrollTop') const line = editor.doc.getCursor().line let top if (line === 0) { top = 0 } else { const blockElements = previewDoc.querySelectorAll('body [data-line]') const blocks = [] for (const block of blockElements) { const l = parseInt(block.getAttribute('data-line')) blocks.push({ line: l, top: block.offsetTop }) if (l > line) { break } } if (blocks.length === 1) { const block = blockElements[blockElements.length - 1] blocks.push({ line: editor.doc.size, top: block.offsetTop + block.offsetHeight }) } const i = blocks.length - 1 const ratio = (blocks[i].top - blocks[i - 1].top) / (blocks[i].line - blocks[i - 1].line) const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3) top = blocks[i - 1].top + Math.floor((line - blocks[i - 1].line) * ratio) - delta } this.scrollTo(previewTop, top, y => _.set(previewDoc, 'body.scrollTop', y) ) } } setValue(value) { this.refs.code.setValue(value) } handleOnChange(e) { this.value = this.refs.code.value this.props.onChange(e) } handleEditorScroll(e) { if (this.userScroll) { const previewDoc = _.get( this, 'refs.preview.refs.root.contentWindow.document' ) const codeDoc = _.get(this, 'refs.code.editor.doc') const from = codeDoc.cm.coordsChar({ left: 0, top: 0 }).line const to = codeDoc.cm.coordsChar({ left: 0, top: codeDoc.cm.display.lastWrapHeight * 1.125 }).line const previewTop = _.get(previewDoc, 'body.scrollTop') let top if (from === 0) { top = 0 } else if (to === codeDoc.lastLine()) { top = _.get(previewDoc, 'body.scrollHeight') - _.get(previewDoc, 'body.clientHeight') } else { const line = from + Math.floor((to - from) / 3) const blockElements = previewDoc.querySelectorAll('body [data-line]') const blocks = [] for (const block of blockElements) { const l = parseInt(block.getAttribute('data-line')) blocks.push({ line: l, top: block.offsetTop }) if (l > line) { break } } if (blocks.length === 1) { const block = blockElements[blockElements.length - 1] blocks.push({ line: codeDoc.size, top: block.offsetTop + block.offsetHeight }) } const i = blocks.length - 1 const ratio = (blocks[i].top - blocks[i - 1].top) / (blocks[i].line - blocks[i - 1].line) top = blocks[i - 1].top + Math.floor((line - blocks[i - 1].line) * ratio) } this.scrollTo(previewTop, top, y => _.set(previewDoc, 'body.scrollTop', y) ) } } handlePreviewScroll(e) { if (this.userScroll) { const previewDoc = _.get( this, 'refs.preview.refs.root.contentWindow.document' ) const codeDoc = _.get(this, 'refs.code.editor.doc') const srcTop = _.get(previewDoc, 'body.scrollTop') const editorTop = _.get(codeDoc, 'scrollTop') let top if (srcTop === 0) { top = 0 } else { const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3) const previewTop = srcTop + delta const blockElements = previewDoc.querySelectorAll('body [data-line]') const blocks = [] for (const block of blockElements) { const top = block.offsetTop blocks.push({ line: parseInt(block.getAttribute('data-line')), top }) if (top > previewTop) { break } } if (blocks.length === 1) { const block = blockElements[blockElements.length - 1] blocks.push({ line: codeDoc.size, top: block.offsetTop + block.offsetHeight }) } const i = blocks.length - 1 const from = codeDoc.cm.heightAtLine(blocks[i - 1].line, 'local') const to = codeDoc.cm.heightAtLine(blocks[i].line, 'local') const ratio = (previewTop - blocks[i - 1].top) / (blocks[i].top - blocks[i - 1].top) top = from + Math.floor((to - from) * ratio) - delta } this.scrollTo(editorTop, top, y => codeDoc.cm.scrollTo(0, y)) } } handleCheckboxClick(e) { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ 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)) { newLine = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { newLine = targetLine.replace(uncheckReplace, '[x]') } this.refs.code.setLineContent(lineIndex, newLine) } } handleMouseMove(e) { if (this.state.isSliderFocused) { const rootRect = this.refs.root.getBoundingClientRect() if (this.props.isStacking) { const rootHeight = rootRect.height const offset = rootRect.top let newCodeEditorHeightInPercent = ((e.pageY - offset) / rootHeight) * 100 // limit minSize to 10%, maxSize to 90% if (newCodeEditorHeightInPercent <= 10) { newCodeEditorHeightInPercent = 10 } if (newCodeEditorHeightInPercent >= 90) { newCodeEditorHeightInPercent = 90 } this.setState({ codeEditorHeightInPercent: newCodeEditorHeightInPercent }) } else { const rootWidth = rootRect.width const offset = rootRect.left let newCodeEditorWidthInPercent = ((e.pageX - offset) / rootWidth) * 100 // limit minSize to 10%, maxSize to 90% if (newCodeEditorWidthInPercent <= 10) { newCodeEditorWidthInPercent = 10 } if (newCodeEditorWidthInPercent >= 90) { newCodeEditorWidthInPercent = 90 } this.setState({ codeEditorWidthInPercent: newCodeEditorWidthInPercent }) } } } handleMouseUp(e) { e.preventDefault() this.setState({ isSliderFocused: false }) } handleMouseDown(e) { e.preventDefault() this.setState({ isSliderFocused: true }) } scrollTo(from, to, scroller) { const distance = to - from const framerate = 1000 / 60 const frames = 20 const refractory = frames * framerate this.userScroll = false let frame = 0 let scrollPos, time const timer = setInterval(() => { time = frame / frames scrollPos = time < 0.5 ? 2 * time * time // ease in : -1 + (4 - 2 * time) * time // ease out scroller(from + scrollPos * distance) if (frame >= frames) { clearInterval(timer) setTimeout(() => { this.userScroll = true }, refractory) } frame++ }, framerate) } render() { const { config, value, storageKey, noteKey, linesHighlighted, getNote, isStacking, RTL } = this.props let storage try { storage = findStorage(storageKey) } catch (e) { return
} let editorStyle = {} let previewStyle = {} let sliderStyle = {} let editorFontSize = parseInt(config.editor.fontSize, 10) if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 editorStyle.fontSize = editorFontSize let editorIndentSize = parseInt(config.editor.indentSize, 10) if (!(editorStyle.fontSize > 0 && editorStyle.fontSize < 132)) editorIndentSize = 4 editorStyle.indentSize = editorIndentSize editorStyle = Object.assign( editorStyle, isStacking ? { width: '100%', height: `${this.state.codeEditorHeightInPercent}%` } : { width: `${this.state.codeEditorWidthInPercent}%`, height: '100%' } ) previewStyle = Object.assign( previewStyle, isStacking ? { width: '100%', height: `${100 - this.state.codeEditorHeightInPercent}%` } : { width: `${100 - this.state.codeEditorWidthInPercent}%`, height: '100%' } ) sliderStyle = Object.assign( sliderStyle, isStacking ? { left: 0, top: `${this.state.codeEditorHeightInPercent}%` } : { left: `${this.state.codeEditorWidthInPercent}%`, top: 0 } ) if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none' return (
this.handleMouseMove(e)} onMouseUp={e => this.handleMouseUp(e)} > this.handleOnChange(e)} onScroll={e => this.handleEditorScroll(e)} onCursorActivity={e => this.handleCursorActivity(e)} spellCheck={config.editor.spellcheck} enableSmartPaste={config.editor.enableSmartPaste} hotkey={config.hotkey} switchPreview={config.editor.switchPreview} enableMarkdownLint={config.editor.enableMarkdownLint} customMarkdownLintConfig={config.editor.customMarkdownLintConfig} dateFormatISO8601={config.editor.dateFormatISO8601} deleteUnusedAttachments={config.editor.deleteUnusedAttachments} RTL={RTL} />
this.handleMouseDown(e)} >
this.handleCheckboxClick(e)} onScroll={e => this.handlePreviewScroll(e)} showCopyNotification={config.ui.showCopyNotification} storagePath={storage.path} noteKey={noteKey} customCSS={config.preview.customCSS} allowCustomCSS={config.preview.allowCustomCSS} lineThroughCheckbox={config.preview.lineThroughCheckbox} getNote={getNote} export={config.export} RTL={RTL} />
) } } export default CSSModules(MarkdownSplitEditor, styles)