From 02e93b06d6bfa4897b6d8e6f126b01763253e180 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 5 Nov 2020 17:30:38 +0100 Subject: [PATCH] Use rich text editor in translations --- .../RichTextEditor/RichTextEditor.tsx | 143 +----------------- .../RichTextEditor/RichTextEditorContent.tsx | 79 ++++++++++ src/components/RichTextEditor/styles.ts | 112 ++++++++++++++ .../stories/pages/PageDetailsPage.tsx | 4 +- .../TranslationFields/TranslationFields.tsx | 3 +- .../TranslationFieldsRich.tsx | 58 +++---- .../TranslationFieldsSave.tsx | 2 +- src/translations/types.ts | 3 +- src/utils/richText/useRichText.ts | 12 +- 9 files changed, 240 insertions(+), 176 deletions(-) create mode 100644 src/components/RichTextEditor/RichTextEditorContent.tsx create mode 100644 src/components/RichTextEditor/styles.ts diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index e85df71a8..f853f205a 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -1,136 +1,23 @@ import EditorJS, { OutputData } from "@editorjs/editorjs"; -import Header from "@editorjs/header"; -import List from "@editorjs/list"; -import Quote from "@editorjs/quote"; import FormControl from "@material-ui/core/FormControl"; import FormHelperText from "@material-ui/core/FormHelperText"; import InputLabel from "@material-ui/core/InputLabel"; -import { makeStyles } from "@material-ui/core/styles"; -import { fade } from "@material-ui/core/styles/colorManipulator"; -import strikethroughIcon from "@saleor/icons/StrikethroughIcon"; import classNames from "classnames"; -import createGenericInlineTool from "editorjs-inline-tool"; import React from "react"; +import { RichTextEditorContentProps, tools } from "./RichTextEditorContent"; +import useStyles from "./styles"; + export type RichTextEditorChange = (data: OutputData) => void; -export interface RichTextEditorProps { - data: OutputData; +export interface RichTextEditorProps extends RichTextEditorContentProps { disabled: boolean; error: boolean; helperText: string; label: string; name: string; onChange: RichTextEditorChange; - onReady?: () => void; } -// TODO: improve dark mode -const useStyles = makeStyles( - theme => { - const hover = { - "&:hover": { - background: fade(theme.palette.primary.main, 0.1) - } - }; - - return { - root: { - "& .cdx-quote__text": { - minHeight: 24 - }, - "& .ce-block--selected .ce-block__content": { - background: `${fade(theme.palette.primary.main, 0.2)} !important` - }, - "& .ce-block__content": { - margin: 0, - maxWidth: "unset" - }, - "& .ce-conversion-tool": { - ...hover - }, - "& .ce-conversion-tool--focused": { - background: `${fade(theme.palette.primary.main, 0.1)} !important` - }, - "& .ce-conversion-tool__icon": { - background: "none" - }, - "& .ce-conversion-toolbar": { - background: theme.palette.background.paper - }, - "& .ce-header": { - marginBottom: 0, - paddingBottom: theme.spacing(1) - }, - "& .ce-inline-tool": { - ...hover, - color: theme.palette.text.primary, - height: 32, - transition: theme.transitions.duration.short + "ms", - width: 32 - }, - "& .ce-inline-toolbar": { - "& input": { - background: "none" - }, - background: theme.palette.background.paper, - color: theme.palette.text.primary - }, - "& .ce-inline-toolbar__dropdown": { - ...hover, - height: 32, - marginRight: 0 - }, - "& .ce-inline-toolbar__toggler-and-button-wrapper": { - paddingRight: 0 - }, - "& .ce-toolbar__actions": { - right: 0, - top: 0 - }, - "& .ce-toolbar__content": { - maxWidth: "unset" - }, - "& .ce-toolbar__plus": { - left: -9 - }, - "& .ce-toolbox.ce-toolbox--opened": { - left: 16 - }, - "& .codex-editor__redactor": { - marginRight: `${theme.spacing(4)}px !important`, - paddingBottom: "0 !important" - }, - "& a": { - color: theme.palette.primary.light - }, - "&:not($rootDisabled):hover": { - borderColor: theme.palette.primary.main - }, - border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`, - borderRadius: 4, - boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`, - fontSize: theme.typography.body1.fontSize, - minHeight: 56, - padding: theme.spacing(3, 2), - paddingBottom: theme.spacing(), - paddingLeft: 10, - position: "relative", - transition: theme.transitions.duration.short + "ms" - }, - rootActive: { - boxShadow: `inset 0px 0px 0 2px ${theme.palette.primary.main}` - }, - rootDisabled: { - ...theme.overrides.MuiOutlinedInput.root["&$disabled"]["& fieldset"] - }, - rootError: { - borderColor: theme.palette.error.main - } - }; - }, - { name: "RichTextEditor" } -); - const RichTextEditor: React.FC = ({ data, disabled, @@ -158,25 +45,7 @@ const RichTextEditor: React.FC = ({ }, onReady, readOnly: disabled, - tools: { - header: { - class: Header, - config: { - defaultLevel: 1, - levels: [1, 2, 3] - } - }, - list: List, - quote: Quote, - strikethrough: createGenericInlineTool({ - sanitize: { - s: {} - }, - shortcut: "CMD+S", - tagName: "s", - toolboxIcon: strikethroughIcon - }) - } + tools }); } @@ -205,7 +74,7 @@ const RichTextEditor: React.FC = ({ {label}
void; +} + +export const tools: Record = { + header: { + class: Header, + config: { + defaultLevel: 1, + levels: [1, 2, 3] + } + }, + list: List, + quote: Quote, + strikethrough: createGenericInlineTool({ + sanitize: { + s: {} + }, + shortcut: "CMD+S", + tagName: "s", + toolboxIcon: strikethroughIcon + }) +}; + +const RichTextEditorContent: React.FC = ({ + className, + data, + onReady +}) => { + const classes = useStyles({}); + + const editor = React.useRef(); + const editorContainer = React.useRef(); + React.useEffect( + () => { + if (data) { + editor.current = new EditorJS({ + data, + holder: editorContainer.current, + onReady, + readOnly: true, + tools + }); + } + + return editor.current?.destroy; + }, + // Rerender editor only if changed from undefined to defined state + [data === undefined] + ); + React.useEffect(() => editor.current?.destroy, []); + + return ( +
+ ); +}; + +RichTextEditorContent.displayName = "RichTextEditorContent"; +export default RichTextEditorContent; diff --git a/src/components/RichTextEditor/styles.ts b/src/components/RichTextEditor/styles.ts new file mode 100644 index 000000000..2436bf2da --- /dev/null +++ b/src/components/RichTextEditor/styles.ts @@ -0,0 +1,112 @@ +import { makeStyles } from "@material-ui/core/styles"; +import { fade } from "@material-ui/core/styles/colorManipulator"; + +const useStyles = makeStyles( + theme => { + const hover = { + "&:hover": { + background: fade(theme.palette.primary.main, 0.1) + } + }; + + return { + editor: { + "& .cdx-quote__text": { + minHeight: 24 + }, + "& .ce-block--selected .ce-block__content": { + background: `${fade(theme.palette.primary.main, 0.2)} !important` + }, + "& .ce-block__content": { + margin: 0, + maxWidth: "unset" + }, + "& .ce-conversion-tool": { + ...hover + }, + "& .ce-conversion-tool--focused": { + background: `${fade(theme.palette.primary.main, 0.1)} !important` + }, + "& .ce-conversion-tool__icon": { + background: "none" + }, + "& .ce-conversion-toolbar": { + background: theme.palette.background.paper + }, + "& .ce-header": { + marginBottom: 0, + paddingBottom: theme.spacing(1) + }, + "& .ce-inline-tool": { + ...hover, + color: theme.palette.text.primary, + height: 32, + transition: theme.transitions.duration.short + "ms", + width: 32 + }, + "& .ce-inline-toolbar": { + "& input": { + background: "none" + }, + background: theme.palette.background.paper, + color: theme.palette.text.primary + }, + "& .ce-inline-toolbar__dropdown": { + ...hover, + height: 32, + marginRight: 0 + }, + "& .ce-inline-toolbar__toggler-and-button-wrapper": { + paddingRight: 0 + }, + "& .ce-toolbar__actions": { + right: 0, + top: 0 + }, + "& .ce-toolbar__content": { + maxWidth: "unset" + }, + "& .ce-toolbar__plus": { + left: -9 + }, + "& .ce-toolbox.ce-toolbox--opened": { + left: 16 + }, + "& .codex-editor__redactor": { + marginRight: `${theme.spacing(4)}px !important`, + paddingBottom: "0 !important" + }, + "& a": { + color: theme.palette.primary.light + }, + "&:not($rootDisabled):hover": { + borderColor: theme.palette.primary.main + } + }, + root: { + border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`, + borderRadius: 4, + boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`, + fontSize: theme.typography.body1.fontSize, + minHeight: 56, + padding: theme.spacing(3, 2), + paddingBottom: theme.spacing(), + paddingLeft: 10, + position: "relative", + transition: theme.transitions.duration.short + "ms" + }, + rootActive: { + boxShadow: `inset 0px 0px 0 2px ${theme.palette.primary.main}` + }, + rootDisabled: { + ...theme.overrides.MuiOutlinedInput.root["&$disabled"]["& fieldset"] + }, + rootError: { + borderColor: theme.palette.error.main + } + }; + }, + { name: "RichTextEditor" } +); + +export default useStyles; diff --git a/src/storybook/stories/pages/PageDetailsPage.tsx b/src/storybook/stories/pages/PageDetailsPage.tsx index 51af7b057..f8814abe9 100644 --- a/src/storybook/stories/pages/PageDetailsPage.tsx +++ b/src/storybook/stories/pages/PageDetailsPage.tsx @@ -1,9 +1,9 @@ +import { PageData } from "@saleor/pages/components/PageDetailsPage/form"; import { PageErrorCode } from "@saleor/types/globalTypes"; import { storiesOf } from "@storybook/react"; import React from "react"; import PageDetailsPage, { - PageDetailsPageFormData, PageDetailsPageProps } from "../../../pages/components/PageDetailsPage"; import { page } from "../../../pages/fixtures"; @@ -36,7 +36,7 @@ storiesOf("Views / Pages / Page details", module) "isPublished", "seoDescription", "seoTitle" - ] as Array).map(field => ({ + ] as Array).map(field => ({ __typename: "PageError", code: PageErrorCode.INVALID, field diff --git a/src/translations/components/TranslationFields/TranslationFields.tsx b/src/translations/components/TranslationFields/TranslationFields.tsx index 1b82272ab..953300a1d 100644 --- a/src/translations/components/TranslationFields/TranslationFields.tsx +++ b/src/translations/components/TranslationFields/TranslationFields.tsx @@ -1,3 +1,4 @@ +import { OutputData } from "@editorjs/editorjs"; import Button from "@material-ui/core/Button"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; @@ -36,7 +37,7 @@ export interface TranslationFieldsProps { saveButtonState: ConfirmButtonTransitionState; onEdit: (field: string) => void; onDiscard: () => void; - onSubmit: (field: string, data: string) => void; + onSubmit: (field: string, data: string | OutputData) => void; } const useStyles = makeStyles( diff --git a/src/translations/components/TranslationFields/TranslationFieldsRich.tsx b/src/translations/components/TranslationFields/TranslationFieldsRich.tsx index 1a940fc45..51dde46b7 100644 --- a/src/translations/components/TranslationFields/TranslationFieldsRich.tsx +++ b/src/translations/components/TranslationFields/TranslationFieldsRich.tsx @@ -1,8 +1,9 @@ +import { OutputData } from "@editorjs/editorjs"; import Typography from "@material-ui/core/Typography"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import DraftRenderer from "@saleor/components/DraftRenderer"; -import Form from "@saleor/components/Form"; import RichTextEditor from "@saleor/components/RichTextEditor"; +import RichTextEditorContent from "@saleor/components/RichTextEditor/RichTextEditorContent"; +import useRichText from "@saleor/utils/richText/useRichText"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -14,7 +15,7 @@ interface TranslationFieldsRichProps { initial: string; saveButtonState: ConfirmButtonTransitionState; onDiscard: () => void; - onSubmit: (data: string) => void; + onSubmit: (data: OutputData) => void; } const TranslationFieldsRich: React.FC = ({ @@ -26,40 +27,39 @@ const TranslationFieldsRich: React.FC = ({ onSubmit }) => { const intl = useIntl(); + const [content, change] = useRichText({ + initial, + triggerChange: () => undefined + }); + + const submit = () => onSubmit(content.current); return edit ? ( -
onSubmit(data.translation)} - > - {({ change, submit }) => ( -
- - -
- )} -
+
+ + + ) : initial === null ? ( ) : ( - + ); }; diff --git a/src/translations/components/TranslationFields/TranslationFieldsSave.tsx b/src/translations/components/TranslationFields/TranslationFieldsSave.tsx index ab2a40b02..c925414c5 100644 --- a/src/translations/components/TranslationFields/TranslationFieldsSave.tsx +++ b/src/translations/components/TranslationFields/TranslationFieldsSave.tsx @@ -43,7 +43,7 @@ const TranslationFieldsSave: React.FC = props => { > -
diff --git a/src/translations/types.ts b/src/translations/types.ts index 8c56c4bf1..335268725 100644 --- a/src/translations/types.ts +++ b/src/translations/types.ts @@ -1,3 +1,4 @@ +import { OutputData } from "@editorjs/editorjs"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ShopInfo_shop_languages } from "@saleor/components/Shop/types/ShopInfo"; @@ -11,5 +12,5 @@ export interface TranslationsEntitiesPageProps { onEdit: (field: string) => void; onDiscard: () => void; onLanguageChange: (lang: string) => void; - onSubmit: (field: string, data: string) => void; + onSubmit: (field: string, data: string | OutputData) => void; } diff --git a/src/utils/richText/useRichText.ts b/src/utils/richText/useRichText.ts index 522e06a08..652e88bc4 100644 --- a/src/utils/richText/useRichText.ts +++ b/src/utils/richText/useRichText.ts @@ -11,11 +11,13 @@ function useRichText(opts: { ); const [, setLoaded] = useState(false); useEffect(() => { - try { - data.current = JSON.parse(opts.initial); - setLoaded(true); - } catch { - data.current = undefined; + if (opts.initial !== null) { + try { + data.current = JSON.parse(opts.initial); + setLoaded(true); + } catch { + data.current = undefined; + } } }, [opts.initial]);