2017-06-01 22:58:09 +00:00
|
|
|
import React, {PropTypes, PureComponent} from 'react';
|
|
|
|
import ReactDOM from 'react-dom';
|
|
|
|
import autobind from 'autobind-decorator';
|
|
|
|
import classnames from 'classnames';
|
|
|
|
import marked from 'marked';
|
|
|
|
import highlight from 'highlight.js';
|
|
|
|
import {Tab, TabList, TabPanel, Tabs} from 'react-tabs';
|
|
|
|
import {trackEvent} from '../../analytics';
|
|
|
|
import Button from './base/button';
|
|
|
|
import CodeEditor from './codemirror/code-editor';
|
|
|
|
import * as misc from '../../common/misc';
|
|
|
|
|
|
|
|
@autobind
|
|
|
|
class MarkdownEditor extends PureComponent {
|
|
|
|
constructor (props) {
|
|
|
|
super(props);
|
|
|
|
this.state = {
|
|
|
|
markdown: props.defaultValue,
|
2017-06-05 15:09:06 +00:00
|
|
|
compiled: '',
|
|
|
|
renderError: ''
|
2017-06-01 22:58:09 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_trackTab (name) {
|
|
|
|
trackEvent('Request', 'Markdown Editor Tab', name);
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleChange (markdown) {
|
|
|
|
this.props.onChange(markdown);
|
|
|
|
this._compileMarkdown(markdown);
|
|
|
|
}
|
|
|
|
|
|
|
|
async _compileMarkdown (markdown) {
|
2017-06-05 15:09:06 +00:00
|
|
|
const newState = {markdown};
|
|
|
|
try {
|
|
|
|
const rendered = await this.props.handleRender(markdown);
|
|
|
|
newState.compiled = marked(rendered);
|
|
|
|
} catch (err) {
|
|
|
|
newState.renderError = err.message;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState(newState);
|
2017-06-01 22:58:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_setPreviewRef (n) {
|
|
|
|
this._preview = n;
|
|
|
|
}
|
|
|
|
|
|
|
|
_highlightCodeBlocks () {
|
|
|
|
if (!this._preview) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const el = ReactDOM.findDOMNode(this._preview);
|
|
|
|
for (const block of el.querySelectorAll('pre > code')) {
|
|
|
|
highlight.highlightBlock(block);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const a of el.querySelectorAll('a')) {
|
|
|
|
a.addEventListener('click', e => {
|
|
|
|
e.preventDefault();
|
|
|
|
misc.clickLink(e.target.getAttribute('href'));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillMount () {
|
|
|
|
this._compileMarkdown(this.state.markdown);
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate () {
|
|
|
|
this._highlightCodeBlocks();
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount () {
|
|
|
|
marked.setOptions({
|
|
|
|
renderer: new marked.Renderer(),
|
|
|
|
gfm: true,
|
|
|
|
tables: true,
|
|
|
|
breaks: false,
|
|
|
|
pedantic: false,
|
2017-06-07 21:36:43 +00:00
|
|
|
sanitize: true,
|
2017-06-01 22:58:09 +00:00
|
|
|
smartLists: true,
|
|
|
|
smartypants: false
|
|
|
|
});
|
|
|
|
|
|
|
|
this._highlightCodeBlocks();
|
|
|
|
}
|
|
|
|
|
|
|
|
render () {
|
|
|
|
const {
|
|
|
|
fontSize,
|
|
|
|
lineWrapping,
|
|
|
|
indentSize,
|
|
|
|
keyMap,
|
|
|
|
placeholder,
|
|
|
|
defaultPreviewMode,
|
|
|
|
className,
|
|
|
|
handleRender,
|
|
|
|
handleGetRenderContext
|
|
|
|
} = this.props;
|
|
|
|
|
2017-06-05 15:09:06 +00:00
|
|
|
const {markdown, compiled, renderError} = this.state;
|
2017-06-01 22:58:09 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Tabs className={classnames('markdown-editor', 'outlined', className)}
|
|
|
|
forceRenderTabPanel
|
|
|
|
selectedIndex={defaultPreviewMode ? 1 : 0}>
|
|
|
|
<TabList>
|
|
|
|
<Tab>
|
|
|
|
<Button onClick={this._trackTab} value="Write">
|
|
|
|
Write
|
|
|
|
</Button>
|
|
|
|
</Tab>
|
|
|
|
<Tab>
|
|
|
|
<Button onClick={this._trackTab} value="Preview">
|
|
|
|
Preview
|
|
|
|
</Button>
|
|
|
|
</Tab>
|
|
|
|
</TabList>
|
|
|
|
<TabPanel className="markdown-editor__edit">
|
|
|
|
<div className="form-control form-control--outlined">
|
|
|
|
<CodeEditor
|
|
|
|
hideGutters
|
|
|
|
hideLineNumbers
|
|
|
|
dynamicHeight
|
|
|
|
manualPrettify
|
|
|
|
noStyleActiveLine
|
|
|
|
mode="text/plain"
|
|
|
|
placeholder={placeholder}
|
|
|
|
debounceMillis={300}
|
|
|
|
keyMap={keyMap}
|
|
|
|
fontSize={fontSize}
|
|
|
|
lineWrapping={lineWrapping}
|
|
|
|
indentSize={indentSize}
|
|
|
|
defaultValue={markdown}
|
|
|
|
render={handleRender}
|
|
|
|
getRenderContext={handleGetRenderContext}
|
|
|
|
onChange={this._handleChange}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="txt-sm italic faint">
|
|
|
|
Styling with Markdown is supported
|
|
|
|
</div>
|
|
|
|
</TabPanel>
|
2017-06-05 15:09:06 +00:00
|
|
|
<TabPanel className="markdown-editor__preview">
|
|
|
|
{renderError && <p className="notice error no-margin">Failed to render: {renderError}</p>}
|
2017-06-08 00:28:23 +00:00
|
|
|
<div className="markdown-editor__preview__content selectable"
|
|
|
|
ref={this._setPreviewRef}
|
2017-06-01 22:58:09 +00:00
|
|
|
dangerouslySetInnerHTML={{__html: compiled}}>
|
|
|
|
{/* Set from above */}
|
|
|
|
</div>
|
|
|
|
</TabPanel>
|
|
|
|
</Tabs>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
MarkdownEditor.propTypes = {
|
|
|
|
// Required
|
|
|
|
onChange: PropTypes.func.isRequired,
|
|
|
|
defaultValue: PropTypes.string.isRequired,
|
|
|
|
fontSize: PropTypes.number.isRequired,
|
|
|
|
indentSize: PropTypes.number.isRequired,
|
|
|
|
keyMap: PropTypes.string.isRequired,
|
|
|
|
lineWrapping: PropTypes.bool.isRequired,
|
|
|
|
handleRender: PropTypes.func.isRequired,
|
|
|
|
handleGetRenderContext: PropTypes.func.isRequired,
|
|
|
|
|
|
|
|
// Optional
|
|
|
|
placeholder: PropTypes.string,
|
|
|
|
defaultPreviewMode: PropTypes.bool,
|
|
|
|
className: PropTypes.string
|
|
|
|
};
|
|
|
|
|
|
|
|
export default MarkdownEditor;
|