Markdown preview class->fc (#4956)

* transform

* remove debounce prop and handle nulls
This commit is contained in:
Jack Kavanagh
2022-07-18 12:12:11 +02:00
committed by GitHub
parent 1bef8ac364
commit ddd09c64eb
6 changed files with 55 additions and 118 deletions

View File

@@ -101,11 +101,13 @@ export class Dropdown extends PureComponent<DropdownProps, State> {
continue;
}
const match = fuzzyMatch(newFilter, listItem.textContent);
const match = fuzzyMatch(newFilter, listItem.textContent || '');
if (!newFilter || match) {
const filterIndex = listItem.getAttribute('data-filter-index');
filterItems.push(parseInt(filterIndex, 10));
if (filterIndex) {
filterItems.push(parseInt(filterIndex, 10));
}
}
}
@@ -131,7 +133,9 @@ export class Dropdown extends PureComponent<DropdownProps, State> {
for (const li of this._dropdownList.querySelectorAll('li')) {
if (li.hasAttribute('data-filter-index')) {
const filterIndex = li.getAttribute('data-filter-index');
items.push(parseInt(filterIndex, 10));
if (filterIndex) {
items.push(parseInt(filterIndex, 10));
}
}
}
}

View File

@@ -15,7 +15,7 @@ import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
import { SetRequired } from 'type-fest';
import { AUTOBIND_CFG, CONTENT_TYPE_JSON, DEBOUNCE_MILLIS } from '../../../../common/constants';
import { AUTOBIND_CFG, CONTENT_TYPE_JSON } from '../../../../common/constants';
import { database as db } from '../../../../common/database';
import { hotKeyRefs } from '../../../../common/hotkeys';
import { executeHotKey } from '../../../../common/hotkeys-listener';
@@ -811,7 +811,6 @@ export class GraphQLEditor extends PureComponent<Props, State> {
dynamicHeight
enableNunjucks
uniquenessKey={uniquenessKey ? uniquenessKey + '::variables' : undefined}
debounceMillis={DEBOUNCE_MILLIS * 4}
manualPrettify={false}
defaultValue={variables}
className={className}

View File

@@ -86,7 +86,6 @@ export class MarkdownEditor extends PureComponent<Props, State> {
enableNunjucks
mode={mode || 'text/x-markdown'}
placeholder={placeholder}
debounceMillis={300}
defaultValue={markdown}
onChange={this._handleChange}
/>

View File

@@ -1,10 +1,7 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import highlight from 'highlight.js/lib/common';
import React, { FC, PureComponent } from 'react';
import ReactDOM from 'react-dom';
import React, { FC, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../common/constants';
import { clickLink } from '../../common/electron-helpers';
import { markdownToHTML } from '../../common/markdown-to-html';
import { HandleRender } from '../../common/render';
@@ -14,122 +11,60 @@ interface Props {
markdown: string;
handleRender?: HandleRender;
className?: string;
debounceMillis?: number;
heading?: string;
}
interface State {
compiled: string;
renderError: string;
}
export const MarkdownPreview: FC<Props> = ({ markdown, className, heading }) => {
const divRef = useRef<HTMLDivElement>(null);
const [compiled, setCompiled] = useState('');
const [error, setError] = useState('');
const { handleRender } = useGatedNunjucks();
@autoBindMethodsForReact(AUTOBIND_CFG)
class MarkdownPreviewInternal 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: string) {
if (this._compileTimeout !== null) {
clearTimeout(this._compileTimeout);
}
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(preview: HTMLDivElement) {
this._preview = preview;
}
_handleClickLink(event: any) {
event.preventDefault();
clickLink(event.target.getAttribute('href'));
}
_highlightCodeBlocks() {
if (!this._preview) {
useEffect(() => {
let shouldUpdate = true;
const fn = async () => {
try {
const rendered = handleRender ? await handleRender(markdown) : markdown;
const compiled = markdownToHTML(rendered);
shouldUpdate && setCompiled(compiled);
shouldUpdate && setError('');
} catch (err) {
shouldUpdate && setCompiled('');
shouldUpdate && setError(err.message);
}
};
fn();
return () => {
shouldUpdate = false;
};
}, [handleRender, markdown]);
useLayoutEffect(() => {
if (!divRef.current) {
return;
}
const el = ReactDOM.findDOMNode(this._preview);
// @ts-expect-error -- TSCONVERSION
for (const block of el.querySelectorAll('pre > code')) {
highlight.highlightElement(block);
for (const block of divRef.current.querySelectorAll('pre > code')) {
if (block instanceof HTMLElement) {
highlight.highlightElement(block);
}
}
// @ts-expect-error -- TSCONVERSION
for (const a of el.querySelectorAll('a')) {
for (const a of divRef.current.querySelectorAll('a')) {
a.title = `Open ${a.getAttribute('href')} in browser`;
a.removeEventListener('click', this._handleClickLink);
a.addEventListener('click', this._handleClickLink);
a.removeEventListener('click', _handleClickLink);
a.addEventListener('click', _handleClickLink);
}
}
}, [compiled]);
const _handleClickLink = (event: any) => {
event.preventDefault();
clickLink(event.target.getAttribute('href'));
};
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;
return (
<div ref={this._setPreviewRef} className={classnames('markdown-preview', className)}>
{renderError && <p className="notice error no-margin">Failed to render: {renderError}</p>}
<div className="selectable">
{heading ? <h1 className="markdown-preview__content-title">{heading}</h1> : null}
<div className="markdown-preview__content" dangerouslySetInnerHTML={{ __html: compiled }} />
</div>
return (
<div ref={divRef} className={classnames('markdown-preview', className)}>
{error ? <p className="notice error no-margin">Failed to render: {error}</p> : null}
<div className="selectable">
{heading ? <h1 className="markdown-preview__content-title">{heading}</h1> : null}
<div className="markdown-preview__content" dangerouslySetInnerHTML={{ __html: compiled }} />
</div>
);
}
}
export const MarkdownPreview: FC<Omit<Props, 'handleRender'>> = props => {
const { handleRender } = useGatedNunjucks();
return <MarkdownPreviewInternal {...props} handleRender={handleRender}/>;
</div>
);
};

View File

@@ -301,7 +301,6 @@ export const RequestPane: FC<Props> = ({
<ErrorBoundary errorClassName="font-error pad text-center">
<MarkdownPreview
heading={request.name}
debounceMillis={1000}
markdown={request.description}
/>
</ErrorBoundary>

View File

@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["DOM.Iterable"],
"experimentalDecorators": true,
"isolatedModules": true,
"jsx": "react",