From efd31ba00d949e53a22289fd4e151238eba22ead Mon Sep 17 00:00:00 2001 From: Martial Maillot Date: Thu, 30 Jan 2025 09:50:06 +0100 Subject: [PATCH] feat(contributions): ajouter des infographies # Conflicts: # targets/frontend/src/components/forms/EditionField/MenuSpecial.tsx --- .../components/forms/EditionField/Editor.tsx | 187 +++++++++++++++++- .../forms/EditionField/MenuInfographic.tsx | 60 ++++++ .../forms/EditionField/MenuSpecial.tsx | 18 +- .../forms/EditionField/extensions/Alert.ts | 2 +- .../EditionField/extensions/Infographic.ts | 182 +++++++++++++++++ .../forms/EditionField/extensions/index.ts | 1 + 6 files changed, 441 insertions(+), 9 deletions(-) create mode 100644 targets/frontend/src/components/forms/EditionField/MenuInfographic.tsx create mode 100644 targets/frontend/src/components/forms/EditionField/extensions/Infographic.ts diff --git a/targets/frontend/src/components/forms/EditionField/Editor.tsx b/targets/frontend/src/components/forms/EditionField/Editor.tsx index 5a26884c7..2b638642a 100644 --- a/targets/frontend/src/components/forms/EditionField/Editor.tsx +++ b/targets/frontend/src/components/forms/EditionField/Editor.tsx @@ -17,7 +17,18 @@ import { DetailsSummary } from "@tiptap-pro/extension-details-summary"; import { DetailsContent } from "@tiptap-pro/extension-details-content"; import { Placeholder } from "@tiptap/extension-placeholder"; import { Link } from "@tiptap/extension-link"; -import { Alert, Title } from "./extensions"; +import { Alert, Infographic, Title } from "./extensions"; +import { MenuInfographic } from "./MenuInfographic"; +import { + Button, + DialogActions, + DialogContentText, + TextField, +} from "@mui/material"; +import DialogTitle from "@mui/material/DialogTitle"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import { NodeSelection } from "@tiptap/pm/state"; export type EditorProps = { label: string; @@ -29,6 +40,35 @@ export type EditorProps = { const emptyHtml = "

"; +type ModeCreation = { + mode: 0; +}; +const Creation: ModeCreation = { mode: 0 }; + +type ModeEdition = { + mode: 1; + infoUrl: string; + pdfUrl: string; + pdfSize: string; +}; +const Edition = ( + infoUrl: string, + pdfUrl: string, + pdfSize: string +): ModeEdition => ({ + mode: 1, + infoUrl, + pdfUrl, + pdfSize, +}); + +type ModeHide = { + mode: -1; +}; +const Hide: ModeHide = { mode: -1 }; + +type Mode = ModeEdition | ModeCreation | ModeHide; + export const Editor = ({ label, content, @@ -39,6 +79,8 @@ export const Editor = ({ const [currentContent, setCurrentContent] = useState(content); const [focus, setFocus] = useState(false); const [isClient, setIsClient] = useState(false); + const [infographicModal, setInfographicModal] = useState(Hide); + const editor = useEditor({ content, editable: !disabled, @@ -75,6 +117,7 @@ export const Editor = ({ }), Alert, Title, + Infographic, ], onUpdate: ({ editor }) => { const html = editor.getHTML(); @@ -98,6 +141,36 @@ export const Editor = ({ editor?.setOptions({ editable: !disabled }); }, [disabled]); + useEffect(() => { + // We need to focus on the infographic to edit it + const handleClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + + if ( + target.tagName === "IMG" && + target.closest(".infographic") && + editor + ) { + const pos = editor.view.posAtDOM( + target.closest(".infographic") as HTMLElement, + 0 + ); + + editor.view.dispatch( + editor.state.tr.setSelection( + NodeSelection.create(editor.state.doc, pos) + ) + ); + editor.commands.focus(); + } + }; + + document.addEventListener("click", handleClick); + return () => { + document.removeEventListener("click", handleClick); + }; + }, [editor]); + return ( <> {isClient && ( @@ -109,8 +182,28 @@ export const Editor = ({ htmlFor={label} > - + { + setInfographicModal(Creation); + }} + /> + { + const node = editor?.state.selection.$from.node(); + if (node?.type.name === "infographic") { + const src = node.attrs.src; + const dataPdf = node.attrs.urlPdf; + const dataPdfSize = node.attrs.pdfSize; + setInfographicModal(Edition(src, dataPdf, dataPdfSize)); + } + }} + onDelete={() => { + editor?.commands.removeInfographic(); + }} + /> )} + { + setInfographicModal(Hide); + }} + PaperProps={{ + component: "form", + onSubmit: (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const { infoUrl, pdfUrl, pdfSize } = Object.fromEntries( + (formData as any).entries() + ); + if (infographicModal.mode === Creation.mode) { + editor + ?.chain() + .focus() + .setInfographic(infoUrl, pdfUrl, pdfSize) + .run(); + } else { + editor?.commands.updateInfographicSrc(infoUrl, pdfUrl, pdfSize); + } + setInfographicModal(Hide); + }, + }} + > + Infographie + + + Veuillez renseigner les informations suivantes pour ajouter une + infographie au document + + + + + + + + + + ); }; @@ -170,6 +349,10 @@ const StyledEditorContent = styled(EditorContent)(() => { backgroundColor: fr.colors.decisions.background.contrast.info.active, borderRadius: "0.6rem", }, + ".infographic": { + marginBottom: "1.6rem", + color: fr.colors.decisions.text.default, + }, li: { p: { margin: "0", diff --git a/targets/frontend/src/components/forms/EditionField/MenuInfographic.tsx b/targets/frontend/src/components/forms/EditionField/MenuInfographic.tsx new file mode 100644 index 000000000..378093433 --- /dev/null +++ b/targets/frontend/src/components/forms/EditionField/MenuInfographic.tsx @@ -0,0 +1,60 @@ +import { Editor, FloatingMenu } from "@tiptap/react"; +import Delete from "@mui/icons-material/Delete"; +import EditIcon from "@mui/icons-material/Edit"; +import { styled } from "@mui/system"; + +export const MenuInfographic = ({ + editor, + onEdit, + onDelete, +}: { + editor: Editor | null; + onEdit: () => void; + onDelete: () => void; +}) => { + return editor ? ( + { + return ( + editor?.state.selection.$from.node()?.type.name === "infographic" && + state.selection.content().content.size > 0 && + editor.isActive("infographic") + ); + }} + > + + + + ) : ( + <> + ); +}; + +const InfographicFloatingMenu = styled(FloatingMenu)` + display: flex; + background-color: #0d0d0d; + padding: 0.2rem; + border-radius: 0.5rem; + + button { + border: none; + background: none; + font-size: 0.85rem; + font-weight: 500; + padding: 0 0.2rem; + opacity: 0.6; + color: #fff; + + &:hover, + &.is-active { + opacity: 1; + } + } +`; diff --git a/targets/frontend/src/components/forms/EditionField/MenuSpecial.tsx b/targets/frontend/src/components/forms/EditionField/MenuSpecial.tsx index c6b4f3ffe..3ae405b4d 100644 --- a/targets/frontend/src/components/forms/EditionField/MenuSpecial.tsx +++ b/targets/frontend/src/components/forms/EditionField/MenuSpecial.tsx @@ -6,10 +6,10 @@ import { } from "@tiptap/react"; import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered"; +import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate"; import GridOnIcon from "@mui/icons-material/GridOn"; import StorageIcon from "@mui/icons-material/Storage"; import { styled } from "@mui/system"; -import InfoIcon from "@mui/icons-material/Info"; import { Node as ProseMirrorNode } from "@tiptap/pm/model"; const tableHTML = ` @@ -25,7 +25,13 @@ const tableHTML = ` `; -export const MenuSpecial = ({ editor }: { editor: Editor | null }) => { +export const MenuSpecial = ({ + editor, + onNewInfographic, +}: { + editor: Editor | null; + onNewInfographic: (editor: Editor) => void; +}) => { const getTextContent = (node: ProseMirrorNode) => { if (editor) { return getText(node, { @@ -110,13 +116,13 @@ export const MenuSpecial = ({ editor }: { editor: Editor | null }) => { ) : ( diff --git a/targets/frontend/src/components/forms/EditionField/extensions/Alert.ts b/targets/frontend/src/components/forms/EditionField/extensions/Alert.ts index 4e75bb67c..62d36314e 100644 --- a/targets/frontend/src/components/forms/EditionField/extensions/Alert.ts +++ b/targets/frontend/src/components/forms/EditionField/extensions/Alert.ts @@ -26,7 +26,7 @@ export const Alert = Node.create({ group: "block", parseHTML() { - return [{ tag: "div" }]; + return [{ tag: "div.alert" }]; }, renderHTML({ HTMLAttributes }) { diff --git a/targets/frontend/src/components/forms/EditionField/extensions/Infographic.ts b/targets/frontend/src/components/forms/EditionField/extensions/Infographic.ts new file mode 100644 index 000000000..1121fcb81 --- /dev/null +++ b/targets/frontend/src/components/forms/EditionField/extensions/Infographic.ts @@ -0,0 +1,182 @@ +import { Node } from "@tiptap/core"; + +export interface InfographicOptions {} + +declare module "@tiptap/core" { + interface Commands { + infographic: { + setInfographic: ( + src: string, + urlPdf: string, + sizePdf: string + ) => ReturnType; + updateInfographicSrc: ( + newSrc: string, + newUrlPdf: string, + newSizePdf: string + ) => ReturnType; + removeInfographic: () => ReturnType; + }; + } +} + +export const Infographic = Node.create({ + name: "infographic", + draggable: true, + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + addAttributes() { + return { + src: { + parseHTML: (element) => + element.querySelector("img")?.getAttribute("src"), + renderHTML: (attributes) => { + return { src: attributes.src }; + }, + }, + urlPdf: { + parseHTML: (element) => + element.querySelector("div.infographic")?.getAttribute("data-pdf"), + renderHTML: (attributes) => { + return { "data-pdf": attributes.urlPdf }; + }, + }, + pdfSize: { + parseHTML: (element) => + element + .querySelector("div.infographic") + ?.getAttribute("data-pdf-size"), + renderHTML: (attributes) => { + return { "data-pdf-size": attributes.pdfSize }; + }, + }, + }; + }, + + content: "block+", + + group: "block", + + parseHTML() { + return [ + { + tag: "div.infographic", + getAttrs: (element) => { + const el = element as HTMLElement; + return { + src: el.querySelector("img")?.getAttribute("src") || "", + urlPdf: el.getAttribute("data-pdf") || "", + pdfSize: el.getAttribute("data-pdf-size") || "", + }; + }, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + { + class: "infographic", + "data-pdf": HTMLAttributes["data-pdf"], + "data-pdf-size": HTMLAttributes["data-pdf-size"], + }, + [ + "img", + { + src: HTMLAttributes.src, + height: "auto", + width: "500", + }, + ], + ["div", {}, 0], + ]; + }, + + addCommands() { + return { + setInfographic: + (src: string, urlPdf: string, pdfSize: string) => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: { src, urlPdf, pdfSize }, + content: [ + { + type: "details", + content: [ + { + type: "detailsSummary", + content: [ + { + type: "text", + text: "Afficher le contenu de l'infographie", + }, + ], + }, + { + type: "detailsContent", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Décrire ici le contenu de l'infographie", + }, + ], + }, + ], + }, + ], + }, + ], + }); + }, + + updateInfographicSrc: + (newSrc: string, newUrlPdf: string, newSizePdf: string) => + ({ state, chain }) => { + const { selection } = state; + const node = selection.$anchor.node(); + + if (node.type.name !== "infographic") { + return false; + } + return chain() + .updateAttributes("infographic", { + src: newSrc, + urlPdf: newUrlPdf, + pdfSize: newSizePdf, + }) + .run(); + }, + + removeInfographic: + () => + ({ state, dispatch }) => { + const { selection } = state; + const node = selection.$anchor.node(); + + if (node.type.name !== "infographic") { + return false; + } + + if (dispatch) { + const tr = state.tr.delete( + selection.$anchor.before(), + selection.$anchor.after() + ); + dispatch(tr); + } + + return true; + }, + }; + }, +}); diff --git a/targets/frontend/src/components/forms/EditionField/extensions/index.ts b/targets/frontend/src/components/forms/EditionField/extensions/index.ts index 7f9a73ca3..c0df52c3d 100644 --- a/targets/frontend/src/components/forms/EditionField/extensions/index.ts +++ b/targets/frontend/src/components/forms/EditionField/extensions/index.ts @@ -1,2 +1,3 @@ export * from "./Alert"; export * from "./Titles"; +export * from "./Infographic";