Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown editor plugins #3457

Merged
merged 2 commits into from
May 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src-docs/src/views/markdown_editor/markdown_editor.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* eslint-disable prettier/prettier */
import React from 'react';
import React, { useState } from 'react';

import { EuiMarkdownEditor } from '../../../../src/components/markdown_editor';

// eslint-disable-next-line
const markdownExample = require('!!raw-loader!./markdown-example.md');

export default () => (
<EuiMarkdownEditor initialValue={markdownExample} height={400} />
);
export default () => {
const [value, setValue] = useState(markdownExample);
return <EuiMarkdownEditor value={value} onChange={setValue} height={400} />;
};
7 changes: 6 additions & 1 deletion src/components/markdown_editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@
* under the License.
*/

export { EuiMarkdownEditor } from './markdown_editor';
export {
EuiMarkdownEditor,
EuiMarkdownEditorProps,
defaultParsingPlugins,
defaultProcessingPlugins,
} from './markdown_editor';
7 changes: 6 additions & 1 deletion src/components/markdown_editor/markdown_editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ import { EuiMarkdownEditor } from './markdown_editor';
describe('EuiMarkdownEditor', () => {
test('is rendered', () => {
const component = render(
<EuiMarkdownEditor editorId="editorId" {...requiredProps} />
<EuiMarkdownEditor
editorId="editorId"
value=""
onChange={() => null}
{...requiredProps}
/>
);

expect(component).toMatchSnapshot();
Expand Down
212 changes: 123 additions & 89 deletions src/components/markdown_editor/markdown_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,112 +17,146 @@
* under the License.
*/

import React, { Component, HTMLAttributes } from 'react';
import React, {
createElement,
FunctionComponent,
HTMLAttributes,
useMemo,
useState,
} from 'react';
import unified, { PluggableList } from 'unified';
import classNames from 'classnames';
// @ts-ignore
import emoji from 'remark-emoji';
import markdown from 'remark-parse';
// @ts-ignore
import remark2rehype from 'remark-rehype';
// @ts-ignore
import highlight from 'remark-highlight.js';
// @ts-ignore
import rehype2react from 'rehype-react';

import { CommonProps } from '../common';
import MarkdownActions from './markdown_actions';
import { EuiMarkdownEditorToolbar } from './markdown_editor_toolbar';
import { EuiMarkdownEditorTextArea } from './markdown_editor_text_area';
import { EuiMarkdownFormat } from './markdown_format';
import { EuiMarkdownEditorDropZone } from './markdown_editor_drop_zone';
import { htmlIdGenerator } from '../../services/accessibility';
import { EuiLink } from '../link';
import { EuiCodeBlock } from '../code';
import { MARKDOWN_MODE, MODE_EDITING, MODE_VIEWING } from './markdown_modes';

function storeMarkdownTree() {
return function(tree: any, file: any) {
file.data.markdownTree = JSON.parse(JSON.stringify(tree));
};
}

export const defaultParsingPlugins: PluggableList = [
[markdown, {}],
[highlight, {}],
[emoji, { emoticon: true }],
[storeMarkdownTree, {}],
];

export const defaultProcessingPlugins: PluggableList = [
[remark2rehype, { allowDangerousHTML: true }],
[
rehype2react,
{
createElement: createElement,
components: {
a: EuiLink,
code: (props: any) =>
// if has classNames is a codeBlock using highlight js
props.className ? (
<EuiCodeBlock {...props} />
) : (
<code className="euiMarkdownFormat__code" {...props} />
),
},
},
],
];

export type EuiMarkdownEditorProps = HTMLAttributes<HTMLDivElement> &
CommonProps & {
/** A unique ID to attach to the textarea. If one isn't provided, a random one
* will be generated */
editorId?: string;
/** A initial markdown content */
initialValue?: string;
/** The height of the content/preview area */
height: number;
};

export interface MarkdownEditorState {
editorContent: string;
viewMarkdownPreview: boolean;
files: FileList | null;
}

export class EuiMarkdownEditor extends Component<
EuiMarkdownEditorProps,
MarkdownEditorState
> {
editorId: string;
markdownActions: MarkdownActions;
/** A markdown content */
value: string;

static defaultProps = {
height: 150,
};
/** Callback function when markdown content is modified */
onChange: (value: string) => void;

constructor(props: EuiMarkdownEditorProps) {
super(props);

this.state = {
editorContent: this.props.initialValue!,
viewMarkdownPreview: false,
files: null,
};

// If an ID wasn't provided, just generate a rando
this.editorId =
this.props.editorId ||
Math.random()
.toString(35)
.substring(2, 10);
this.markdownActions = new MarkdownActions(this.editorId);

this.handleMdButtonClick = this.handleMdButtonClick.bind(this);
}

handleMdButtonClick = (mdButtonId: string) => {
this.markdownActions.do(mdButtonId);
};
/** The height of the content/preview area */
height?: number;

onClickPreview = () => {
this.setState({ viewMarkdownPreview: !this.state.viewMarkdownPreview });
};
/** array of unified plugins to parse content into an AST */
parsingPluginList?: PluggableList;

onAttachFiles = (files: FileList | null) => {
console.log('List of attached files -->', files);
this.setState({
files: files,
});
/** array of unified plugins to convert the AST into a ReactNode */
processingPluginList?: PluggableList;
};

render() {
const { className, editorId, initialValue, height, ...rest } = this.props;

const { viewMarkdownPreview } = this.state;

const classes = classNames('euiMarkdownEditor', className);

return (
<div className={classes} {...rest}>
<EuiMarkdownEditorToolbar
markdownActions={this.markdownActions}
onClickPreview={this.onClickPreview}
viewMarkdownPreview={viewMarkdownPreview}
/>

{this.state.viewMarkdownPreview ? (
<div
className="euiMarkdownEditor__previewContainer"
style={{ height: `${height}px` }}>
<EuiMarkdownFormat>{this.state.editorContent}</EuiMarkdownFormat>
</div>
) : (
<EuiMarkdownEditorDropZone>
<EuiMarkdownEditorTextArea
height={height}
id={this.editorId}
onChange={(e: any) => {
this.setState({ editorContent: e.target.value });
}}
value={this.state.editorContent}
/>
</EuiMarkdownEditorDropZone>
)}
</div>
);
}
}
export const EuiMarkdownEditor: FunctionComponent<EuiMarkdownEditorProps> = ({
className,
editorId: _editorId,
value,
onChange,
height = 150,
parsingPluginList = defaultParsingPlugins,
processingPluginList = defaultProcessingPlugins,
...rest
}) => {
const [viewMode, setViewMode] = useState<MARKDOWN_MODE>(MODE_EDITING);
const editorId = useMemo(() => _editorId || htmlIdGenerator()(), [_editorId]);

const markdownActions = useMemo(() => new MarkdownActions(editorId), [
editorId,
]);

const classes = classNames('euiMarkdownEditor', className);

const processor = useMemo(
() =>
unified()
.use(parsingPluginList)
.use(processingPluginList),
[parsingPluginList, processingPluginList]
);

const isPreviewing = viewMode === MODE_VIEWING;

return (
<div className={classes} {...rest}>
<EuiMarkdownEditorToolbar
markdownActions={markdownActions}
onClickPreview={() =>
setViewMode(isPreviewing ? MODE_EDITING : MODE_VIEWING)
}
viewMode={viewMode}
/>

{isPreviewing ? (
<div
className="euiMarkdownEditor__previewContainer"
style={{ height: `${height}px` }}>
<EuiMarkdownFormat processor={processor}>{value}</EuiMarkdownFormat>
</div>
) : (
<EuiMarkdownEditorDropZone>
<EuiMarkdownEditorTextArea
height={height}
id={editorId}
onChange={e => onChange(e.target.value)}
value={value}
/>
</EuiMarkdownEditorDropZone>
)}
</div>
);
};
15 changes: 9 additions & 6 deletions src/components/markdown_editor/markdown_editor_toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import { EuiButtonEmpty, EuiButtonIcon } from '../button';
import { EuiFlexItem, EuiFlexGroup } from '../flex';
import { EuiI18n } from '../i18n';
import { EuiToolTip } from '../tool_tip';
import { MARKDOWN_MODE, MODE_VIEWING } from './markdown_modes';

export type EuiMarkdownEditorToolbarProps = HTMLAttributes<HTMLDivElement> &
CommonProps & {
markdownActions?: any;
viewMarkdownPreview?: boolean;
viewMode?: MARKDOWN_MODE;
onClickPreview?: any;
};

Expand Down Expand Up @@ -90,7 +91,9 @@ export class EuiMarkdownEditorToolbar extends Component<
};

render() {
const { viewMarkdownPreview, onClickPreview } = this.props;
const { viewMode, onClickPreview } = this.props;

const isPreviewing = viewMode === MODE_VIEWING;

return (
<div className="euiMarkdownEditor__toolbar">
Expand All @@ -108,7 +111,7 @@ export class EuiMarkdownEditorToolbar extends Component<
onClick={() => this.handleMdButtonClick(item.id)}
iconType={item.iconType}
aria-label={item.label}
isDisabled={viewMarkdownPreview}
isDisabled={isPreviewing}
/>
</EuiToolTip>
))}
Expand All @@ -120,7 +123,7 @@ export class EuiMarkdownEditorToolbar extends Component<
onClick={() => this.handleMdButtonClick(item.id)}
iconType={item.iconType}
aria-label={item.label}
isDisabled={viewMarkdownPreview}
isDisabled={isPreviewing}
/>
</EuiToolTip>
))}
Expand All @@ -132,15 +135,15 @@ export class EuiMarkdownEditorToolbar extends Component<
onClick={() => this.handleMdButtonClick(item.id)}
iconType={item.iconType}
aria-label={item.label}
isDisabled={viewMarkdownPreview}
isDisabled={isPreviewing}
/>
</EuiToolTip>
))}
</EuiFlexItem>

<EuiFlexItem grow={false}>
{/* The idea was to use the EuiButtonToggle but it doesn't work when pressing the enter key */}
{viewMarkdownPreview ? (
{isPreviewing ? (
<EuiButtonEmpty
iconType="editorCodeBlock"
color="text"
Expand Down
50 changes: 11 additions & 39 deletions src/components/markdown_editor/markdown_format.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,21 @@
* under the License.
*/

import React, { createElement, FunctionComponent } from 'react';
// @ts-ignore
import emoji from 'remark-emoji';
import unified from 'unified';
import markdown from 'remark-parse';
// @ts-ignore
import remark2rehype from 'remark-rehype';
// @ts-ignore
import highlight from 'remark-highlight.js';
// @ts-ignore
import rehype2react from 'rehype-react';

import { EuiCodeBlock } from '../code/code_block';
import { EuiLink } from '../link/link';

const processor = unified()
.use(markdown)
.use(highlight)
.use(emoji, { emoticon: true })
.use(remark2rehype, { allowDangerousHTML: true })
// .use(row)
.use(rehype2react, {
createElement: createElement,
components: {
a: EuiLink,
code: (props: any) =>
// if has classNames is a codeBlock using highlight js
props.className ? (
<EuiCodeBlock {...props} />
) : (
<code className="euiMarkdownFormat__code" {...props} />
),
},
});
import React, { FunctionComponent, useMemo } from 'react';
import { Processor } from 'unified';

interface EuiMarkdownFormatProps {
children: string;
processor: Processor;
}

export const EuiMarkdownFormat: FunctionComponent<EuiMarkdownFormatProps> = ({
children,
}) => (
<div className="euiMarkdownFormat">
{processor.processSync(children).contents}
</div>
);
processor,
}) => {
const result = useMemo(() => processor.processSync(children), [
processor,
children,
]);
return <div className="euiMarkdownFormat">{result.contents}</div>;
};
Loading