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

317 lines
9 KiB
TypeScript
Raw Normal View History

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";
import Typography from "@material-ui/core/Typography";
import { CreateCSSProperties } from "@material-ui/styles/withStyles";
import { ChangeEvent } from "@saleor/hooks/useForm";
2019-08-09 11:14:35 +00:00
import classNames from "classnames";
2019-06-19 14:40:52 +00:00
import { RawDraftContentState } from "draft-js";
import {
BLOCK_TYPE,
DraftailEditor,
ENTITY_TYPE,
INLINE_STYLE
} from "draftail";
2019-08-09 11:14:35 +00:00
import isEqual from "lodash-es/isEqual";
2019-08-09 10:26:22 +00:00
import React from "react";
import ErrorBoundary from "react-error-boundary";
import { FormattedMessage } from "react-intl";
2019-06-19 14:40:52 +00:00
import BoldIcon from "../../icons/BoldIcon";
import HeaderOne from "../../icons/HeaderOne";
2019-12-02 10:49:14 +00:00
import HeaderThree from "../../icons/HeaderThree";
import HeaderTwo from "../../icons/HeaderTwo";
2019-06-19 14:40:52 +00:00
import ItalicIcon from "../../icons/ItalicIcon";
import LinkIcon from "../../icons/LinkIcon";
import OrderedListIcon from "../../icons/OrderedListIcon";
import QuotationIcon from "../../icons/QuotationIcon";
import StrikethroughIcon from "../../icons/StrikethroughIcon";
import UnorderedListIcon from "../../icons/UnorderedListIcon";
import LinkEntity from "./LinkEntity";
import LinkSource from "./LinkSource";
export interface RichTextEditorProps {
disabled: boolean;
error: boolean;
helperText: string;
2019-12-02 10:49:14 +00:00
initial?: RawDraftContentState;
2019-06-19 14:40:52 +00:00
label: string;
name: string;
scroll?: boolean;
onChange: (event: React.ChangeEvent<any>) => void;
}
2019-12-03 15:28:40 +00:00
const useStyles = makeStyles(
theme => {
2020-02-03 11:20:01 +00:00
const editorContainer: CreateCSSProperties = {
border: `1px ${theme.palette.divider} solid`,
borderRadius: 4,
padding: "27px 12px 10px",
position: "relative",
transition: theme.transitions.duration.shortest + "ms"
};
return {
editorContainer,
error: {
color: theme.palette.error.main
2019-06-19 14:40:52 +00:00
},
helperText: {
marginTop: theme.spacing(0.75)
},
input: {
position: "relative"
},
label: {
fontSize: theme.typography.caption.fontSize,
left: 12,
position: "absolute",
top: 9
},
linkIcon: {
marginTop: 2
},
root: {
"& .DraftEditor": {
"&-editorContainer": {
"& .public-DraftEditor-content": {
lineHeight: 1.62
},
"& a": {
color: theme.palette.primary.light
},
"&:after": {
background: theme.palette.getContrastText(
theme.palette.background.default
),
bottom: -11,
content: "''",
display: "block",
height: 2,
left: -12,
position: "absolute",
transform: "scaleX(0) scaleY(0)",
width: "calc(100% + 24px)"
},
position: "relative"
2019-12-03 15:28:40 +00:00
},
"&-root": {
...theme.typography.body1
}
2019-12-03 15:28:40 +00:00
},
"& .Draftail": {
"&-Editor": {
"&--focus": {
boxShadow: `inset 0px 0px 0px 2px ${theme.palette.primary.main}`
2019-12-03 15:28:40 +00:00
},
"&:hover": {
borderColor: theme.palette.primary.main
},
...editorContainer
},
"&-Toolbar": {
"&Button": {
"& svg": {
padding: 2
},
"&--active": {
"&:hover": {
background: theme.palette.primary.main
},
"&:not(:hover)": {
borderRightColor: theme.palette.primary.main
},
2019-12-03 15:28:40 +00:00
background: theme.palette.primary.main
},
"&:focus": {
"&:active": {
"&:after": {
background: fade(theme.palette.primary.main, 0.3),
borderRadius: "100%",
content: "''",
display: "block",
height: "100%",
width: "100%"
}
}
},
"&:hover": {
background: fade(theme.palette.primary.main, 0.3)
2019-12-03 15:28:40 +00:00
},
background: "none",
border: "none",
borderRight: `1px ${theme.palette.divider} solid`,
color: theme.typography.body1.color,
cursor: "pointer",
display: "inline-flex",
height: 36,
justifyContent: "center",
padding: theme.spacing(1) + 2,
transition: theme.transitions.duration.short + "ms",
width: 36
2019-06-19 14:40:52 +00:00
},
"&Group": {
"&:last-of-type": {
"& .Draftail-ToolbarButton": {
"&:last-of-type": {
border: "none"
}
2019-12-03 15:28:40 +00:00
}
},
display: "flex"
2019-06-19 14:40:52 +00:00
},
background: theme.palette.background.default,
border: `1px ${theme.palette.divider} solid`,
2019-12-03 15:28:40 +00:00
display: "inline-flex",
flexWrap: "wrap",
marginBottom: theme.spacing(),
marginTop: 10,
[theme.breakpoints.down(460)]: {
width: "min-content"
}
2019-10-30 14:34:24 +00:00
},
"&-block": {
"&--blockquote": {
borderLeft: `2px solid ${theme.palette.divider}`,
margin: 0,
padding: theme.spacing(1, 2)
}
2019-12-03 15:28:40 +00:00
}
2019-10-30 14:34:24 +00:00
},
"&$error": {
"& .Draftail": {
"&-Editor": {
borderColor: theme.palette.error.main
}
2019-12-03 15:28:40 +00:00
}
2019-06-19 14:40:52 +00:00
}
},
scroll: {
"& .DraftEditor": {
"&-editorContainer": {
"& .public-DraftEditor-content": {
lineHeight: 1.62
}
2019-12-03 15:28:40 +00:00
}
2019-06-19 14:40:52 +00:00
}
},
smallIcon: {
marginLeft: 10
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
function handleSave(
value: any,
initial: any,
name: string,
onChange: (event: ChangeEvent) => void
) {
if (value && !isEqual(value, initial)) {
onChange({
target: {
name,
value
}
});
}
}
2019-10-30 14:34:24 +00:00
const RichTextEditor: React.FC<RichTextEditorProps> = props => {
const { error, helperText, initial, label, name, scroll, onChange } = props;
const classes = useStyles(props);
return (
2019-06-19 14:40:52 +00:00
<div
className={classNames({
[classes.error]: error,
[classes.root]: true,
[classes.scroll]: scroll
})}
>
<div className={classes.input}>
<Typography className={classes.label} variant="caption" color="primary">
{label}
</Typography>
<ErrorBoundary
FallbackComponent={() => (
<div className={classes.editorContainer}>
<Typography color="error">
<FormattedMessage
defaultMessage="Invalid content"
description="rich text error"
/>
</Typography>
</div>
)}
>
<DraftailEditor
key={JSON.stringify(initial)}
rawContentState={
initial && Object.keys(initial).length > 0 ? initial : null
2019-06-19 14:40:52 +00:00
}
onSave={value => handleSave(value, initial, name, onChange)}
blockTypes={[
{
icon: <HeaderOne />,
type: BLOCK_TYPE.HEADER_ONE
},
{ icon: <HeaderTwo />, type: BLOCK_TYPE.HEADER_TWO },
{ icon: <HeaderThree />, type: BLOCK_TYPE.HEADER_THREE },
{ icon: <QuotationIcon />, type: BLOCK_TYPE.BLOCKQUOTE },
{
icon: <UnorderedListIcon />,
type: BLOCK_TYPE.UNORDERED_LIST_ITEM
},
{ icon: <OrderedListIcon />, type: BLOCK_TYPE.ORDERED_LIST_ITEM }
]}
inlineStyles={[
{
icon: <BoldIcon className={classes.smallIcon} />,
type: INLINE_STYLE.BOLD
},
{
icon: <ItalicIcon className={classes.smallIcon} />,
type: INLINE_STYLE.ITALIC
},
{
icon: <StrikethroughIcon />,
type: INLINE_STYLE.STRIKETHROUGH
}
]}
enableLineBreak
entityTypes={[
{
attributes: ["url"],
decorator: LinkEntity,
icon: <LinkIcon className={classes.linkIcon} />,
source: LinkSource,
type: ENTITY_TYPE.LINK
}
]}
/>
</ErrorBoundary>
2019-06-19 14:40:52 +00:00
</div>
{helperText && (
<Typography
className={classNames({
[classes.error]: error,
[classes.helperText]: true
})}
variant="caption"
>
{helperText}
</Typography>
)}
</div>
2019-10-30 14:34:24 +00:00
);
};
2019-06-19 14:40:52 +00:00
RichTextEditor.displayName = "RichTextEditor";
RichTextEditor.defaultProps = {
scroll: true
};
export default RichTextEditor;