saleor-dashboard/src/components/RichTextEditor/RichTextEditor.tsx

224 lines
6.1 KiB
TypeScript
Raw Normal View History

2020-10-28 15:26:30 +00:00
import EditorJS, { OutputData } from "@editorjs/editorjs";
import Header from "@editorjs/header";
import List from "@editorjs/list";
import Quote from "@editorjs/quote";
2020-11-05 13:56:29 +00:00
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import InputLabel from "@material-ui/core/InputLabel";
2019-10-30 14:34:24 +00:00
import { makeStyles } from "@material-ui/core/styles";
2019-06-19 14:40:52 +00:00
import { fade } from "@material-ui/core/styles/colorManipulator";
2020-10-28 15:26:30 +00:00
import strikethroughIcon from "@saleor/icons/StrikethroughIcon";
2019-08-09 11:14:35 +00:00
import classNames from "classnames";
2020-10-28 15:30:44 +00:00
import createGenericInlineTool from "editorjs-inline-tool";
2019-08-09 10:26:22 +00:00
import React from "react";
2019-06-19 14:40:52 +00:00
2020-11-03 11:35:36 +00:00
export type RichTextEditorChange = (data: OutputData) => void;
2019-06-19 14:40:52 +00:00
export interface RichTextEditorProps {
2020-11-03 11:35:36 +00:00
data: OutputData;
2019-06-19 14:40:52 +00:00
disabled: boolean;
error: boolean;
helperText: string;
label: string;
name: string;
2020-11-03 11:35:36 +00:00
onChange: RichTextEditorChange;
2020-11-03 13:53:17 +00:00
onReady?: () => void;
2019-06-19 14:40:52 +00:00
}
2020-11-04 12:56:04 +00:00
// TODO: improve dark mode
2019-12-03 15:28:40 +00:00
const useStyles = makeStyles(
theme => {
2020-10-28 15:26:30 +00:00
const hover = {
"&:hover": {
background: fade(theme.palette.primary.main, 0.1)
}
};
return {
root: {
2020-10-28 15:26:30 +00:00
"& .cdx-quote__text": {
minHeight: 24
2019-12-03 15:28:40 +00:00
},
2020-11-05 14:10:46 +00:00
"& .ce-block--selected .ce-block__content": {
2020-11-05 14:31:00 +00:00
background: `${fade(theme.palette.primary.main, 0.2)} !important`
2020-11-05 14:10:46 +00:00
},
2020-10-29 12:14:05 +00:00
"& .ce-block__content": {
margin: 0,
maxWidth: "unset"
},
2020-10-28 15:26:30 +00:00
"& .ce-conversion-tool": {
...hover
2019-10-30 14:34:24 +00:00
},
2020-10-28 15:26:30 +00:00
"& .ce-conversion-tool--focused": {
background: `${fade(theme.palette.primary.main, 0.1)} !important`
},
2020-11-05 14:10:46 +00:00
"& .ce-conversion-tool__icon": {
background: "none"
},
"& .ce-conversion-toolbar": {
background: theme.palette.background.paper
},
2020-11-04 12:48:58 +00:00
"& .ce-header": {
marginBottom: 0,
paddingBottom: theme.spacing(1)
},
2020-10-28 15:26:30 +00:00
"& .ce-inline-tool": {
...hover,
2020-11-05 14:10:46 +00:00
color: theme.palette.text.primary,
2020-10-28 15:26:30 +00:00
height: 32,
transition: theme.transitions.duration.short + "ms",
width: 32
},
2020-11-05 14:10:46 +00:00
"& .ce-inline-toolbar": {
"& input": {
background: "none"
},
background: theme.palette.background.paper,
color: theme.palette.text.primary
},
2020-10-28 15:26:30 +00:00
"& .ce-inline-toolbar__dropdown": {
...hover,
height: 32,
marginRight: 0
},
"& .ce-inline-toolbar__toggler-and-button-wrapper": {
paddingRight: 0
},
2020-11-03 16:17:37 +00:00
"& .ce-toolbar__actions": {
right: 0,
top: 0
},
"& .ce-toolbar__content": {
maxWidth: "unset"
},
"& .ce-toolbar__plus": {
left: -9
},
"& .ce-toolbox.ce-toolbox--opened": {
left: 16
},
2020-10-28 15:26:30 +00:00
"& .codex-editor__redactor": {
marginRight: `${theme.spacing(4)}px !important`,
paddingBottom: "0 !important"
},
"& a": {
color: theme.palette.primary.light
},
2020-11-05 13:56:29 +00:00
"&:not($rootDisabled):hover": {
2020-10-28 15:26:30 +00:00
borderColor: theme.palette.primary.main
},
2020-10-29 12:14:05 +00:00
border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`,
borderRadius: 4,
2020-10-28 15:26:30 +00:00
boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`,
2020-11-03 13:53:17 +00:00
fontSize: theme.typography.body1.fontSize,
2020-11-05 13:56:29 +00:00
minHeight: 56,
2020-10-28 15:26:30 +00:00
padding: theme.spacing(3, 2),
2020-11-03 13:53:17 +00:00
paddingBottom: theme.spacing(),
paddingLeft: 10,
2020-10-29 12:14:05 +00:00
position: "relative",
2020-10-28 15:26:30 +00:00
transition: theme.transitions.duration.short + "ms"
},
2020-10-28 15:26:30 +00:00
rootActive: {
boxShadow: `inset 0px 0px 0 2px ${theme.palette.primary.main}`
2020-11-05 13:56:29 +00:00
},
rootDisabled: {
...theme.overrides.MuiOutlinedInput.root["&$disabled"]["& fieldset"]
},
rootError: {
borderColor: theme.palette.error.main
2019-06-19 14:40:52 +00:00
}
};
},
2019-12-03 15:28:40 +00:00
{ name: "RichTextEditor" }
);
2019-08-09 11:14:35 +00:00
2020-10-28 15:26:30 +00:00
const RichTextEditor: React.FC<RichTextEditorProps> = ({
2020-11-03 11:35:36 +00:00
data,
2020-11-05 13:56:29 +00:00
disabled,
2020-10-28 15:26:30 +00:00
error,
helperText,
2020-11-03 11:35:36 +00:00
label,
2020-11-05 13:56:29 +00:00
name,
2020-11-03 13:53:17 +00:00
onChange,
onReady
2020-10-28 15:26:30 +00:00
}) => {
const classes = useStyles({});
2019-10-30 14:34:24 +00:00
2020-10-28 15:26:30 +00:00
const [isFocused, setFocus] = React.useState(false);
const editor = React.useRef<EditorJS>();
const editorContainer = React.useRef<HTMLDivElement>();
2020-11-03 11:35:36 +00:00
React.useEffect(
() => {
if (data) {
editor.current = new EditorJS({
data,
holder: editorContainer.current,
onChange: async api => {
const savedData = await api.saver.save();
onChange(savedData);
2020-10-28 15:26:30 +00:00
},
2020-11-03 13:53:17 +00:00
onReady,
2020-11-05 13:56:29 +00:00
readOnly: disabled,
2020-11-03 11:35:36 +00:00
tools: {
header: {
class: Header,
config: {
defaultLevel: 1,
levels: [1, 2, 3]
}
},
list: List,
quote: Quote,
strikethrough: createGenericInlineTool({
sanitize: {
2020-11-05 14:16:39 +00:00
s: {}
2020-11-03 11:35:36 +00:00
},
shortcut: "CMD+S",
tagName: "s",
toolboxIcon: strikethroughIcon
})
}
});
2020-10-28 15:26:30 +00:00
}
2020-11-03 11:35:36 +00:00
return editor.current?.destroy;
},
// Rerender editor only if changed from undefined to defined state
[data === undefined]
);
React.useEffect(() => editor.current?.destroy, []);
2020-11-05 13:56:29 +00:00
React.useEffect(() => {
if (editor.current?.readOnly) {
editor.current.readOnly.toggle(disabled);
}
}, [disabled]);
2019-10-30 14:34:24 +00:00
return (
2020-11-05 13:56:29 +00:00
<FormControl
data-test="richTextEditor"
data-test-id={name}
disabled={disabled}
error={error}
fullWidth
variant="outlined"
>
<InputLabel focused={true} shrink={true}>
{label}
</InputLabel>
2020-10-28 15:26:30 +00:00
<div
className={classNames(classes.root, {
2020-11-05 13:56:29 +00:00
[classes.rootActive]: isFocused,
[classes.rootDisabled]: disabled,
[classes.rootError]: error
2020-10-28 15:26:30 +00:00
})}
ref={editorContainer}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
2020-11-05 13:56:29 +00:00
/>
<FormHelperText>{helperText}</FormHelperText>
</FormControl>
2019-10-30 14:34:24 +00:00
);
};
2019-06-19 14:40:52 +00:00
RichTextEditor.displayName = "RichTextEditor";
export default RichTextEditor;