Use editorjs instead of draftail

This commit is contained in:
dominik-zeglen 2020-10-28 16:26:30 +01:00
parent f3b399830c
commit dab8064e26
6 changed files with 245 additions and 361 deletions

44
package-lock.json generated
View file

@ -1949,6 +1949,35 @@
}
}
},
"@editorjs/editorjs": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.19.0.tgz",
"integrity": "sha512-8PUVaBZx69IrG8dNrE+FZbHSiRTR8ql8L/cmEi1mOdEdTqnOLq5Wv9dgemK00mBWEgNoavMAjtGQpItGknAa8A==",
"requires": {
"codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.1"
}
},
"@editorjs/header": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.6.0.tgz",
"integrity": "sha512-1psNX/irDjJ8Bp1l7DjkYWz7IBtjVIRANk7kPkNoY2CfAeeCFYbJmMlXdqTF2WeAjYv2WMy5ey/aR5fTccgFaw=="
},
"@editorjs/image": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@editorjs/image/-/image-2.6.0.tgz",
"integrity": "sha512-lX4Pz9cW3gGFzlmYLRAsBXTiqUG/MRG7NK4QVU+n/VnUWPU1e791eiIpgRLHfpPj6Maaw5a+GRut90D5EdXtqg=="
},
"@editorjs/list": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@editorjs/list/-/list-1.6.0.tgz",
"integrity": "sha512-2oJ3Nj3lDcIKS6GcrHYHzUUabIjg7zlXTYXQWdEWXevbnM0/fq+4psyI/AYtqbaa3jN+bycPBIW4OG3zD+3d5A=="
},
"@editorjs/quote": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@editorjs/quote/-/quote-2.4.0.tgz",
"integrity": "sha512-IWOBWjL2ngPP63GcIAltyD9kc7OVZFma4kS+T5JRHvKKDspYsnmrxsbRmCPc+coZQzqPxXHkiOZuNMdmGX/Y3w=="
},
"@emotion/cache": {
"version": "10.0.19",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.19.tgz",
@ -8778,6 +8807,16 @@
"urlgrey": "0.4.4"
}
},
"codex-notifier": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/codex-notifier/-/codex-notifier-1.1.2.tgz",
"integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg=="
},
"codex-tooltip": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.1.tgz",
"integrity": "sha512-1xLb1NZbxguNtf02xBRhDphq/EXvMMeEbY0ievjQTHqf8UjXsD41evGk9rqcbjpl+JOjNgtwnp1OaU/X/h6fhQ=="
},
"coffeescript": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz",
@ -10306,6 +10345,11 @@
"safer-buffer": "^2.1.0"
}
},
"editorjs-inline-tool": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/editorjs-inline-tool/-/editorjs-inline-tool-0.4.0.tgz",
"integrity": "sha512-Ppb4e8IFPjWuNcoNM4tg9bDSo7FgMYAlqP4UhuV5W2JoJBubV5pUcpLrFrSyGTt1HJVEpbrib134zf4wxO+7VA=="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",

View file

@ -17,6 +17,11 @@
"npm": ">=6.11.0"
},
"dependencies": {
"@editorjs/editorjs": "^2.19.0",
"@editorjs/header": "^2.6.0",
"@editorjs/image": "^2.6.0",
"@editorjs/list": "^1.6.0",
"@editorjs/quote": "^2.4.0",
"@material-ui/core": "^4.5.1",
"@material-ui/icons": "^4.5.1",
"@material-ui/styles": "^4.5.2",
@ -36,6 +41,7 @@
"draft-js": "^0.10.5",
"draftail": "^1.2.1",
"draftjs-to-html": "^0.9.1",
"editorjs-inline-tool": "^0.4.0",
"fast-array-diff": "^0.2.0",
"fuzzaldrin": "^2.1.0",
"graphql": "^14.4.2",

View file

@ -1,298 +1,163 @@
import EditorJS, { OutputData } from "@editorjs/editorjs";
import Header from "@editorjs/header";
import List from "@editorjs/list";
import Quote from "@editorjs/quote";
import { makeStyles } from "@material-ui/core/styles";
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";
import { FormChange } from "@saleor/hooks/useForm";
import strikethroughIcon from "@saleor/icons/StrikethroughIcon";
import classNames from "classnames";
import { RawDraftContentState } from "draft-js";
import {
BLOCK_TYPE,
DraftailEditor,
ENTITY_TYPE,
INLINE_STYLE
} from "draftail";
import isEqual from "lodash-es/isEqual";
import createGenericInlineTool, {
ItalicInlineTool,
UnderlineInlineTool
} from "editorjs-inline-tool";
import React from "react";
import ErrorBoundary from "react-error-boundary";
import { FormattedMessage } from "react-intl";
import BoldIcon from "../../icons/BoldIcon";
import HeaderOne from "../../icons/HeaderOne";
import HeaderThree from "../../icons/HeaderThree";
import HeaderTwo from "../../icons/HeaderTwo";
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;
initial?: RawDraftContentState;
initial: OutputData;
label: string;
name: string;
scroll?: boolean;
onChange: (event: React.ChangeEvent<any>) => void;
onChange: FormChange;
}
const useStyles = makeStyles(
theme => {
const editorContainer: CreateCSSProperties = {
border: `1px ${theme.palette.divider} solid`,
borderRadius: 4,
padding: "27px 12px 10px",
position: "relative",
transition: theme.transitions.duration.shortest + "ms"
const hover = {
"&:hover": {
background: fade(theme.palette.primary.main, 0.1)
}
};
return {
editorContainer,
error: {
color: theme.palette.error.main
},
helperText: {
marginTop: theme.spacing(0.75)
},
input: {
position: "relative"
},
label: {
fontSize: theme.typography.caption.fontSize,
left: 12,
color: theme.palette.text.secondary,
position: "absolute",
top: 9
top: theme.spacing(4),
transition: theme.transitions.duration.short + "ms"
},
linkIcon: {
marginTop: 2
labelActive: {
color: theme.palette.primary.main
},
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"
},
"&-root": {
...theme.typography.body1
}
"& .cdx-quote__text": {
minHeight: 24
},
"& .Draftail": {
"&-Editor": {
"&--focus": {
boxShadow: `inset 0px 0px 0px 2px ${theme.palette.primary.main}`
},
"&: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
},
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)
},
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
},
"&Group": {
"&:last-of-type": {
"& .Draftail-ToolbarButton": {
"&:last-of-type": {
border: "none"
}
}
},
display: "flex"
},
background: theme.palette.background.default,
border: `1px ${theme.palette.divider} solid`,
display: "inline-flex",
flexWrap: "wrap",
marginBottom: theme.spacing(),
marginTop: 10,
[theme.breakpoints.down(460)]: {
width: "min-content"
}
},
"&-block": {
"&--blockquote": {
borderLeft: `2px solid ${theme.palette.divider}`,
margin: 0,
padding: theme.spacing(1, 2)
}
}
"& .ce-conversion-tool": {
...hover
},
"&$error": {
"& .Draftail": {
"&-Editor": {
borderColor: theme.palette.error.main
}
}
}
"& .ce-conversion-tool--focused": {
background: `${fade(theme.palette.primary.main, 0.1)} !important`
},
"& .ce-inline-tool": {
...hover,
height: 32,
transition: theme.transitions.duration.short + "ms",
width: 32
},
"& .ce-inline-toolbar__dropdown": {
...hover,
height: 32,
marginRight: 0
},
"& .ce-inline-toolbar__toggler-and-button-wrapper": {
paddingRight: 0
},
"& .codex-editor__redactor": {
marginRight: `${theme.spacing(4)}px !important`,
paddingBottom: "0 !important"
},
"& a": {
color: theme.palette.primary.light
},
"&:hover": {
borderColor: theme.palette.primary.main
},
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`,
padding: theme.spacing(3, 2),
transition: theme.transitions.duration.short + "ms"
},
scroll: {
"& .DraftEditor": {
"&-editorContainer": {
"& .public-DraftEditor-content": {
lineHeight: 1.62
}
}
}
},
smallIcon: {
marginLeft: 10
rootActive: {
boxShadow: `inset 0px 0px 0 2px ${theme.palette.primary.main}`
}
};
},
{ name: "RichTextEditor" }
);
function handleSave(
value: any,
initial: any,
name: string,
onChange: (event: ChangeEvent) => void
) {
if (value && !isEqual(value, initial)) {
onChange({
target: {
name,
value
class NewEditor extends EditorJS {}
const RichTextEditor: React.FC<RichTextEditorProps> = ({
error,
helperText,
initial,
label,
name,
onChange
}) => {
const classes = useStyles({});
const [isFocused, setFocus] = React.useState(false);
const editor = React.useRef<EditorJS>();
const editorContainer = React.useRef<HTMLDivElement>();
React.useEffect(() => {
editor.current = new NewEditor({
data: initial,
holder: editorContainer.current,
tools: {
header: {
class: Header,
config: {
defaultLevel: 1,
levels: [1, 2, 3]
}
},
list: List,
quote: Quote,
strikethrough: createGenericInlineTool({
sanitize: {
s: true
},
shortcut: "CMD+S",
tagName: "s",
toolboxIcon: strikethroughIcon
})
}
});
}
}
const RichTextEditor: React.FC<RichTextEditorProps> = props => {
const { error, helperText, initial, label, name, scroll, onChange } = props;
const classes = useStyles(props);
}, []);
React.useEffect(() => () => editor.current.destroy(), []);
return (
<div
className={classNames({
[classes.error]: error,
[classes.root]: true,
[classes.scroll]: scroll
})}
>
<div className={classes.input}>
<Typography className={classes.label} variant="caption" color="primary">
<div>
<div
className={classNames(classes.root, {
[classes.rootActive]: isFocused
})}
ref={editorContainer}
data-test="richTextEditor"
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
>
<Typography
className={classNames(classes.label, {
[classes.labelActive]: isFocused
})}
variant="caption"
>
{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
}
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>
</div>
{helperText && (
<Typography
@ -310,7 +175,4 @@ const RichTextEditor: React.FC<RichTextEditorProps> = props => {
};
RichTextEditor.displayName = "RichTextEditor";
RichTextEditor.defaultProps = {
scroll: true
};
export default RichTextEditor;

View file

@ -1,11 +1,3 @@
import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import React from "react";
const Strikethrough = `<svg height="32" width="32" viewBox="-4 -1 24 16"><path d="M6.53333 14H10.2667V11.2H6.53333V14ZM1.86667 0V2.8H6.53333V5.6H10.2667V2.8H14.9333V0H1.86667ZM0 9.33333H16.8V7.46667H0V9.33333Z"></path></svg>`;
const HeaderOne = createSvgIcon(
<>
<path d="M6.53333 14H10.2667V11.2H6.53333V14ZM1.86667 0V2.8H6.53333V5.6H10.2667V2.8H14.9333V0H1.86667ZM0 9.33333H16.8V7.46667H0V9.33333Z" />
</>,
"HeaderOne"
);
export default HeaderOne;
export default Strikethrough;

View file

@ -1,108 +1,14 @@
import { OutputData } from "@editorjs/editorjs";
import RichTextEditor from "@saleor/components/RichTextEditor";
import { storiesOf } from "@storybook/react";
import { RawDraftContentState } from "draft-js";
import React from "react";
import CardDecorator from "../../CardDecorator";
import Decorator from "../../Decorator";
import * as fixtures from "./fixtures.json";
export const content: OutputData = fixtures.richTextEditor;
export const content: RawDraftContentState = {
blocks: [
{
data: {},
depth: 0,
entityRanges: [],
inlineStyleRanges: [{ length: 4, offset: 0, style: "BOLD" }],
key: "rosn",
text: "bold",
type: "unstyled"
},
{
data: {},
depth: 0,
entityRanges: [],
inlineStyleRanges: [{ length: 6, offset: 0, style: "ITALIC" }],
key: "6tbch",
text: "italic",
type: "unstyled"
},
{
data: {},
depth: 0,
entityRanges: [],
inlineStyleRanges: [{ length: 13, offset: 0, style: "STRIKETHROUGH" }],
key: "1p044",
text: "strikethrough",
type: "unstyled"
},
{
data: {},
depth: 0,
entityRanges: [],
inlineStyleRanges: [],
key: "aven6",
text: "h1",
type: "header-one"
},
{
data: {},
depth: 0,
entityRanges: [],
inlineStyleRanges: [],
key: "9rabl",
text: "h2",
type: "header-two"
},
{
data: {},
depth: 0,
entityRanges: [],
inlineStyleRanges: [],
key: "bv0ac",
text: "h3",
type: "header-three"
},
{
data: {},
depth: 0,
entityRanges: [],
inlineStyleRanges: [],
key: "2ip7q",
text: "blockquote",
type: "blockquote"
},
{
data: {},
depth: 0,
entityRanges: [],
inlineStyleRanges: [],
key: "8r8ss",
text: "ul",
type: "unordered-list-item"
},
{
data: {},
depth: 0,
entityRanges: [],
inlineStyleRanges: [],
key: "911hc",
text: "ol",
type: "ordered-list-item"
},
{
data: {},
depth: 0,
entityRanges: [{ key: 0, length: 4, offset: 0 }],
inlineStyleRanges: [],
key: "5aejo",
text: "link",
type: "unstyled"
}
],
entityMap: {
"0": { data: { url: "#" }, mutability: "MUTABLE", type: "LINK" }
}
};
storiesOf("Generics / Rich text editor", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
@ -110,7 +16,7 @@ storiesOf("Generics / Rich text editor", module)
<RichTextEditor
disabled={false}
error={false}
helperText={""}
helperText="Lorem ipsum dolor sit amet, consectetur adipiscing elit"
initial={content}
label="Content"
name="content"

View file

@ -0,0 +1,74 @@
{
"richTextEditor": {
"time": 1603898483525,
"blocks": [
{ "type": "header", "data": { "text": "Lorem ipsum&nbsp;", "level": 1 } },
{
"type": "paragraph",
"data": {
"text": "Dolor sit amet, consectetur adipiscing elit. <b>Sed iaculis urna et justo accumsan</b>, eget porta est egestas. Nunc odio libero, pharetra in tristique eget, pellentesque in lectus. Sed sed laoreet orci. Suspendisse dui nibh, iaculis ac dui posuere, placerat elementum dolor. In sit amet aliquet nibh. Maecenas sed felis sed lectus gravida vulputate et a mi. Sed a tristique neque, ut euismod arcu. <i>Donec quis aliquet massa.</i> Curabitur arcu purus, facilisis quis posuere sit amet, pharetra at erat."
}
},
{
"type": "list",
"data": {
"style": "ordered",
"items": [
"Maecenas pretium aliquam odio, a iaculis diam dictum ut.",
"Vestibulum pulvinar, quam quis sollicitudin luctus, libero odio laoreet lectus, in tristique ligula dui et ex.<br>",
"Nam quis nibh sed elit fermentum interdum non eget quam."
]
}
},
{
"type": "paragraph",
"data": {
"text": "Nulla sit amet cursus augue, in maximus tellus. <a href=\"http://lipsum.com\">Donec sit amet mollis neque</a>, eget commodo odio."
}
},
{ "type": "header", "data": { "text": "Morbi aliquam", "level": 2 } },
{
"type": "paragraph",
"data": {
"text": "Aliquam posuere nisi et ante malesuada egestas. Phasellus auctor risus a erat aliquam, tempus volutpat arcu rutrum. Duis bibendum id justo ut commodo. Suspendisse imperdiet tincidunt blandit."
}
},
{ "type": "header", "data": { "text": "Nam ipsum purus", "level": 3 } },
{
"type": "paragraph",
"data": {
"text": "Nam ipsum purus, feugiat ut dapibus at, porttitor eget leo. Phasellus sodales urna quis mi viverra, non mollis magna tristique. <s>Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.</s> Pellentesque viverra est sit amet nisi hendrerit, pharetra vehicula neque volutpat. Maecenas feugiat a nulla id blandit. Sed sagittis tempus libero non dignissim. In lectus felis, mattis vitae lacinia nec, volutpat eu elit. Proin ultricies lacus id felis placerat mollis. Integer ultricies eros nec mauris interdum, sit amet sodales ipsum elementum. Vivamus quis dapibus turpis, eu dignissim quam."
}
},
{
"type": "quote",
"data": {
"text": "Nam facilisis augue vel urna tristique rutrum id et tortor.",
"caption": "Morbi erat mi",
"alignment": "left"
}
},
{
"type": "header",
"data": { "text": "Tempor ac posuere nec", "level": 3 }
},
{
"type": "paragraph",
"data": {
"text": "Rhoncus ac lectus. Etiam viverra nisl feugiat tempus eleifend. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae."
}
},
{
"type": "list",
"data": {
"style": "unordered",
"items": [
"Phasellus nec ipsum non metus vestibulum semper",
"In tincidunt, dui vitae suscipit sodales, lacus justo porttitor nulla<br>"
]
}
}
],
"version": "2.19.0"
}
}