insomnia/packages/insomnia-app/app/ui/components/markdown-preview.tsx
Dimitri Mitropoulos 5f4c19da35
[TypeScript] Phase 1 & 2 (#3370)
Co-authored-by: Opender Singh <opender.singh@konghq.com>
2021-05-12 18:35:00 +12:00

136 lines
3.5 KiB
TypeScript

import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
import classnames from 'classnames';
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { AUTOBIND_CFG } from '../../common/constants';
import highlight from 'highlight.js';
import { markdownToHTML } from '../../common/markdown-to-html';
import { HandleRender } from '../../common/render';
import { clickLink } from '../../common/electron-helpers';
interface Props {
markdown: string,
handleRender?: HandleRender,
className?: string,
debounceMillis?: number,
heading?: string,
}
interface State {
compiled: string;
renderError: string;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
class MarkdownPreview extends PureComponent<Props, State> {
state: State = {
compiled: '',
renderError: '',
}
_compileTimeout: NodeJS.Timeout | null = null;
_preview: HTMLDivElement | null = null;
/**
* Debounce and compile the markdown (won't debounce first render)
*/
_compileMarkdown(markdown) {
if (this._compileTimeout !== null) {
clearTimeout(this._compileTimeout);
}
// @ts-expect-error -- TSCONVERSION
this._compileTimeout = setTimeout(
async () => {
try {
const { handleRender } = this.props;
const rendered = handleRender ? await handleRender(markdown) : markdown;
const compiled = markdownToHTML(rendered);
this.setState({
compiled,
renderError: '',
});
} catch (err) {
this.setState({
renderError: err.message,
compiled: '',
});
}
},
this.state.compiled ? this.props.debounceMillis : 0,
);
}
_setPreviewRef(n: HTMLDivElement) {
this._preview = n;
}
_handleClickLink(e) {
e.preventDefault();
clickLink(e.target.getAttribute('href'));
}
_highlightCodeBlocks() {
if (!this._preview) {
return;
}
const el = ReactDOM.findDOMNode(this._preview);
// @ts-expect-error -- TSCONVERSION
for (const block of el.querySelectorAll('pre > code')) {
highlight.highlightBlock(block);
}
// @ts-expect-error -- TSCONVERSION
for (const a of el.querySelectorAll('a')) {
a.title = `Open ${a.getAttribute('href')} in browser`;
a.removeEventListener('click', this._handleClickLink);
a.addEventListener('click', this._handleClickLink);
}
}
componentWillUnmount() {
if (this._compileTimeout !== null) {
clearTimeout(this._compileTimeout);
}
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this._compileMarkdown(this.props.markdown);
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(nextProps: Props) {
this._compileMarkdown(nextProps.markdown);
}
componentDidUpdate() {
this._highlightCodeBlocks();
}
componentDidMount() {
this._highlightCodeBlocks();
}
render() {
const { className, heading } = this.props;
const { compiled, renderError } = this.state;
const html = heading ? `<h1>${heading}</h1>\n${compiled}` : compiled;
return (
<div ref={this._setPreviewRef} className={classnames('markdown-preview', className)}>
{renderError && <p className="notice error no-margin">Failed to render: {renderError}</p>}
<div
className="markdown-preview__content selectable"
dangerouslySetInnerHTML={{
__html: html,
}}>
{/* Set from above */}
</div>
</div>
);
}
}
export default MarkdownPreview;