Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit ced2d16

Browse files
authored
Merge pull request #109 from yoution/feature/tuieditor
feat: add tui editor
2 parents b4d6661 + a1ead8e commit ced2d16

File tree

11 files changed

+310
-2
lines changed

11 files changed

+310
-2
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"dependencies": {
5757
"@popperjs/core": "^2.5.4",
5858
"@reach/router": "^1.3.4",
59+
"@toast-ui/editor": "^2.5.1",
5960
"axios": "^0.21.0",
6061
"classnames": "^2.2.6",
6162
"express": "^4.17.1",

src/components/FormField/index.jsx

+11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Field } from "react-final-form";
99
import { FORM_FIELD_TYPE } from "../../constants";
1010
import TextInput from "../../components/TextInput";
1111
import TextArea from "../../components/TextArea";
12+
import MarkdownEditor from "../../components/MarkdownEditor";
1213
import ReactSelect from "../../components/ReactSelect";
1314
import DateInput from "../../components/DateInput";
1415
import "./styles.module.scss";
@@ -59,6 +60,16 @@ const FormField = ({ field }) => {
5960
step={field.step}
6061
/>
6162
)}
63+
{field.type === FORM_FIELD_TYPE.MARKDOWNEDITOR && (
64+
<MarkdownEditor
65+
placeholder={field.placeholder}
66+
value={input?.value ?? ""}
67+
onChange={input.onChange}
68+
onBlur={input.onBlur}
69+
onFocus={input.onFocus}
70+
className={meta.error && meta.touched ? "error" : ""}
71+
/>
72+
)}
6273
{field.type === FORM_FIELD_TYPE.TEXTAREA && (
6374
<TextArea
6475
placeholder={field.placeholder}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* MarkdownEditor
3+
*/
4+
5+
import React, { useState, useCallback, useRef } from "react";
6+
import PropTypes from "prop-types";
7+
import cn from "classnames";
8+
import TuiEditor from "../TuiEditor";
9+
import styles from "./styles.module.scss";
10+
11+
const MarkdownEditor = (props) => {
12+
const editorElement = useRef(null);
13+
14+
const onChange = useCallback(() => {
15+
const mk = editorElement.current.editorInst.getMarkdown();
16+
props.onChange(mk);
17+
}, []);
18+
19+
return (
20+
<div className={cn(styles["editor-container"], props.className)}>
21+
<TuiEditor
22+
{...props}
23+
ref={editorElement}
24+
onChange={onChange}
25+
initialValue={props.value}
26+
/>
27+
</div>
28+
);
29+
};
30+
31+
MarkdownEditor.propTypes = {
32+
value: PropTypes.string,
33+
className: PropTypes.string,
34+
onChange: PropTypes.func,
35+
onFocus: PropTypes.func,
36+
onBlur: PropTypes.func,
37+
};
38+
39+
export default MarkdownEditor;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@import "styles/include";
2+
3+
.editor-container {
4+
margin-top: 10px;
5+
6+
:global {
7+
// reset style for heading list in headings selection popup
8+
.tui-popup-body {
9+
h1,h2,h3,h4,h4,h5,h6 {
10+
font-weight: revert;
11+
line-height: revert;
12+
}
13+
}
14+
15+
// reset border color
16+
.tui-editor-defaultUI {
17+
border: 1px solid #aaaaab
18+
}
19+
20+
.te-toolbar-section {
21+
border-bottom: 1px solid #aaaaab
22+
}
23+
24+
// hide uplodd file
25+
.tui-editor-popup{
26+
box-shadow: 0px 0px 15px 5px rgba(0,0,0,0.26);
27+
}
28+
29+
.te-popup-add-image .te-tab button, .te-popup-add-image .te-file-type{
30+
display: none !important;
31+
}
32+
33+
.te-popup-add-image .te-url-type{
34+
display: block !important;
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* MarkdownViewer
3+
*/
4+
5+
import React, { useState } from "react";
6+
import PropTypes from "prop-types";
7+
import TuiEditorViewer from "../TuiEditorViewer";
8+
9+
const MarkdownViewer = (props) => (
10+
<TuiEditorViewer initialValue={props.value} />
11+
);
12+
13+
MarkdownViewer.propTypes = {
14+
value: PropTypes.string,
15+
};
16+
17+
export default MarkdownViewer;

src/components/TuiEditor/index.jsx

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* TuiEditor
3+
* wrap toast-ui editor with react
4+
*/
5+
import React from "react";
6+
import PropTypes from "prop-types";
7+
import Editor from "@toast-ui/editor";
8+
9+
class TuiEditor extends React.Component {
10+
constructor(props) {
11+
super(props);
12+
this.rootEl = React.createRef();
13+
this.editorInst = null;
14+
}
15+
16+
getRootElement() {
17+
return this.rootEl.current;
18+
}
19+
20+
getInstance() {
21+
return this.editorInst;
22+
}
23+
24+
bindEventHandlers(props) {
25+
Object.keys(this.props)
26+
.filter((key) => /^on[A-Z][a-zA-Z]+/.test(key))
27+
.forEach((key) => {
28+
const eventName = key[2].toLowerCase() + key.slice(3);
29+
// off function has issue
30+
// when add `onFocus` function, the headings popup will not hide automatically
31+
// this.editorInst.off(eventName, props[key]);
32+
this.editorInst.on(eventName, props[key]);
33+
});
34+
}
35+
36+
componentDidMount() {
37+
this.editorInst = new Editor({
38+
el: this.rootEl.current,
39+
...this.props,
40+
});
41+
42+
this.bindEventHandlers(this.props);
43+
}
44+
45+
shouldComponentUpdate(nextProps) {
46+
const instance = this.getInstance();
47+
const { height, previewStyle } = nextProps;
48+
49+
if (this.props.height !== height) {
50+
instance.height(height);
51+
}
52+
53+
if (this.props.previewStyle !== previewStyle) {
54+
instance.changePreviewStyle(previewStyle);
55+
}
56+
57+
this.bindEventHandlers(nextProps, this.props);
58+
59+
return false;
60+
}
61+
62+
render() {
63+
return <div ref={this.rootEl} />;
64+
}
65+
}
66+
67+
TuiEditor.defaultProps = {
68+
height: "320px",
69+
minHeight: "320px",
70+
initialValue: "",
71+
previewStyle: "",
72+
initialEditType: "wysiwyg",
73+
language: "en-US",
74+
useCommandShortcut: true,
75+
customHTMLSanitizer: null,
76+
frontMatter: false,
77+
hideModeSwitch: true,
78+
referenceDefinition: false,
79+
usageStatistics: false,
80+
useDefaultHTMLSanitizer: true,
81+
};
82+
83+
TuiEditor.propTypes = {
84+
// Editor's initial value
85+
initialValue: PropTypes.string,
86+
// Markdown editor's preview style (tab, vertical)
87+
previewStyle: PropTypes.string.isRequired,
88+
// Editor's height style value. Height is applied as border-box ex) '300px', '100%', 'auto'
89+
height: PropTypes.string,
90+
// Initial editor type (markdown, wysiwyg)
91+
initialEditType: PropTypes.string,
92+
// Editor's min-height style value in pixel ex) '300px'
93+
minHeight: PropTypes.string,
94+
// The placeholder text of the editable element.
95+
placeholder: PropTypes.string,
96+
// hide mode switch tab bar
97+
hideModeSwitch: PropTypes.bool,
98+
// language, 'en-US'
99+
language: PropTypes.string,
100+
// whether use keyboard shortcuts to perform commands
101+
useCommandShortcut: PropTypes.bool,
102+
// It would be emitted when editor fully load1
103+
onLoad: PropTypes.func,
104+
// It would be emitted when content changed
105+
onChange: PropTypes.func,
106+
// It would be emitted when format change by cursor position
107+
onStateChange: PropTypes.func,
108+
// It would be emitted when editor get focus
109+
onFocus: PropTypes.func,
110+
// It would be emitted when editor loose focus
111+
onBlur: PropTypes.func,
112+
// hooks
113+
hooks: PropTypes.arrayOf(PropTypes.object),
114+
// send hostname to google analytics
115+
usageStatistics: PropTypes.bool,
116+
// use default htmlSanitizer
117+
useDefaultHTMLSanitizer: PropTypes.bool,
118+
// toolbar items.
119+
toolbarItems: PropTypes.arrayOf(PropTypes.object),
120+
// Array of plugins. A plugin can be either a function or an array in the form of [function, options].
121+
plugins: PropTypes.arrayOf(PropTypes.object),
122+
// Using extended Autolinks specified in GFM spec
123+
extendedAutolinks: PropTypes.object,
124+
// convertor extention
125+
customConvertor: PropTypes.object,
126+
// Attributes of anchor element that should be rel, target, contenteditable, hreflang, type
127+
linkAttribute: PropTypes.object,
128+
// Object containing custom renderer functions correspond to markdown node
129+
customHTMLRenderer: PropTypes.object,
130+
// whether use the specification of link reference definition
131+
referenceDefinition: PropTypes.bool,
132+
// custom HTML sanitizer
133+
customHTMLSanitizer: PropTypes.func,
134+
// whether use the front matter
135+
frontMatter: PropTypes.bool,
136+
};
137+
138+
export default TuiEditor;
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* TuiEditorViewer
3+
*/
4+
import React from "react";
5+
import PropTypes from "prop-types";
6+
import Viewer from "@toast-ui/editor/dist/toastui-editor-viewer";
7+
8+
class TuiViewer extends React.Component {
9+
constructor(props) {
10+
super(props);
11+
this.rootEl = React.createRef();
12+
this.viewerInst = null;
13+
}
14+
15+
getRootElement() {
16+
return this.rootEl.current;
17+
}
18+
19+
getInstance() {
20+
return this.viewerInst;
21+
}
22+
23+
bindEventHandlers(props) {
24+
Object.keys(this.props)
25+
.filter((key) => /^on[A-Z][a-zA-Z]+/.test(key))
26+
.forEach((key) => {
27+
const eventName = key[2].toLowerCase() + key.slice(3);
28+
// off function has issue
29+
// when add `onFocus` function, the headings popup will not hide automatically
30+
// this.editorInst.off(eventName, props[key]);
31+
this.viewerInst.on(eventName, props[key]);
32+
});
33+
}
34+
35+
componentDidMount() {
36+
this.viewerInst = new Viewer({
37+
el: this.rootEl.current,
38+
...this.props,
39+
});
40+
41+
this.bindEventHandlers(this.props);
42+
}
43+
44+
shouldComponentUpdate(nextProps) {
45+
this.bindEventHandlers(nextProps, this.props);
46+
47+
return false;
48+
}
49+
50+
render() {
51+
return <div ref={this.rootEl} />;
52+
}
53+
}
54+
55+
TuiViewer.propTypes = {
56+
initialValue: PropTypes.string,
57+
};
58+
59+
export default TuiViewer;

src/constants/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export const ACTION_TYPE = {
209209
export const FORM_FIELD_TYPE = {
210210
TEXT: "text",
211211
TEXTAREA: "textarea",
212+
MARKDOWNEDITOR: "markdowneditor",
212213
NUMBER: "number",
213214
SELECT: "select",
214215
MULTISELECT: "multiselect",

src/routes/JobDetails/index.jsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useData } from "hooks/useData";
1212
import { getJobById } from "services/jobs";
1313
import { getSkills } from "services/skills";
1414
import LoadingIndicator from "../../components/LoadingIndicator";
15+
import MarkdownEditorViewer from "../../components/MarkdownEditorViewer";
1516
import withAuthentication from "../../hoc/withAuthentication";
1617
import DataItem from "../../components/DataItem";
1718
import IconSkill from "../../assets/images/icon-skill.svg";
@@ -64,7 +65,7 @@ const JobDetails = ({ teamId, jobId }) => {
6465
{job.title}
6566
</DataItem>
6667
<DataItem title="Job Description" icon={<IconDescription />}>
67-
{job.description}
68+
<MarkdownEditorViewer value={job.description} />
6869
</DataItem>
6970
<DataItem title="Number of Openings" icon={<IconOpenings />}>
7071
{job.numPositions}

src/routes/JobForm/utils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const getEditJobConfig = (skillOptions, onSubmit) => {
4242
},
4343
{
4444
label: "Job Description",
45-
type: FORM_FIELD_TYPE.TEXTAREA,
45+
type: FORM_FIELD_TYPE.MARKDOWNEDITOR,
4646
name: "description",
4747
placeholder: "Job Description",
4848
},

src/styles/main.vendor.scss

+4
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@
44
@import "~react-redux-toastr/src/styles/index";
55
@import "~react-responsive-modal/styles";
66
@import "~react-loader-spinner/dist/loader/css/react-spinner-loader.css";
7+
8+
// toast-ui.editor styles
9+
@import "~codemirror/lib/codemirror.css";
10+
@import "~@toast-ui/editor/dist/toastui-editor.css"

0 commit comments

Comments
 (0)