import Lock from "@mui/icons-material/Lock"; import LockOpen from "@mui/icons-material/LockOpen"; import TextFields from "@mui/icons-material/TextFields"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import type { EditorOptions } from "@tiptap/core"; import { useCallback, useRef, useState } from "react"; import { LinkBubbleMenu, MenuButton, RichTextEditor, RichTextReadOnly, TableBubbleMenu, insertImages, type RichTextEditorRef, } from "../"; import EditorMenuControls from "./EditorMenuControls"; import useExtensions from "./useExtensions"; const exampleContent = '

Hey there đź‘‹

This is a basic example of mui-tiptap, which combines Tiptap with customizable MUI (Material-UI) styles, plus a suite of additional components and extensions! Sure, there are all kinds of text formatting options you’d probably expect from a rich text editor. But wait until you see the @Axl Rose mentions and lists:

Isn’t that great? And all of that is editable. But wait, there’s more! Let’s try a code block:

body {\n  display: none;\n}

That’s only the tip of the iceberg. Feel free to add and resize images:

random image

Organize information in tables:

Name

Role

Team

Alice

PM

Internal tools

Bob

Software

Infrastructure

Or write down your groceries:

Wow, that’s amazing. Good work! 👏
— Mom

Give it a try and click around!

'; function fileListToImageFiles(fileList: FileList): File[] { // You may want to use a package like attr-accept // (https://www.npmjs.com/package/attr-accept) to restrict to certain file // types. return Array.from(fileList).filter((file) => { const mimeType = (file.type || "").toLowerCase(); return mimeType.startsWith("image/"); }); } type Props = { disableStickyMenuBar?: boolean; }; export default function Editor({ disableStickyMenuBar }: Props) { const extensions = useExtensions({ placeholder: "Add your own content here...", }); const rteRef = useRef(null); const [isEditable, setIsEditable] = useState(true); const [showMenuBar, setShowMenuBar] = useState(true); const handleNewImageFiles = useCallback( (files: File[], insertPosition?: number): void => { if (!rteRef.current?.editor) { return; } // For the sake of a demo, we don't have a server to upload the files to, // so we'll instead convert each one to a local "temporary" object URL. // This will not persist properly in a production setting. You should // instead upload the image files to your server, or perhaps convert the // images to bas64 if you would like to encode the image data directly // into the editor content, though that can make the editor content very // large. You will probably want to use the same upload function here as // for the MenuButtonImageUpload `onUploadFiles` prop. const attributesForImageFiles = files.map((file) => ({ src: URL.createObjectURL(file), alt: file.name, })); insertImages({ images: attributesForImageFiles, editor: rteRef.current.editor, position: insertPosition, }); }, [], ); // Allow for dropping images into the editor const handleDrop: NonNullable = useCallback( (view, event, _slice, _moved) => { if (!(event instanceof DragEvent) || !event.dataTransfer) { return false; } const imageFiles = fileListToImageFiles(event.dataTransfer.files); if (imageFiles.length > 0) { const insertPosition = view.posAtCoords({ left: event.clientX, top: event.clientY, })?.pos; handleNewImageFiles(imageFiles, insertPosition); // Return true to treat the event as handled. We call preventDefault // ourselves for good measure. event.preventDefault(); return true; } return false; }, [handleNewImageFiles], ); // Allow for pasting images const handlePaste: NonNullable = useCallback( (_view, event, _slice) => { if (!event.clipboardData) { return false; } const pastedImageFiles = fileListToImageFiles( event.clipboardData.files, ); if (pastedImageFiles.length > 0) { handleNewImageFiles(pastedImageFiles); // Return true to mark the paste event as handled. This can for // instance prevent redundant copies of the same image showing up, // like if you right-click and copy an image from within the editor // (in which case it will be added to the clipboard both as a file and // as HTML, which Tiptap would otherwise separately parse.) return true; } // We return false here to allow the standard paste-handler to run. return false; }, [handleNewImageFiles], ); const [submittedContent, setSubmittedContent] = useState(""); return ( <> } RichTextFieldProps={{ // The "outlined" variant is the default (shown here only as // example), but can be changed to "standard" to remove the outlined // field border from the editor variant: "outlined", MenuBarProps: { hide: !showMenuBar, disableSticky: disableStickyMenuBar, }, // Below is an example of adding a toggle within the outlined field // for showing/hiding the editor menu bar, and a "submit" button for // saving/viewing the HTML content footer: ( theme.palette.divider, py: 1, px: 1.5, }} > { setShowMenuBar((currentState) => !currentState); }} selected={showMenuBar} IconComponent={TextFields} /> { setIsEditable((currentState) => !currentState); }} selected={!isEditable} IconComponent={isEditable ? Lock : LockOpen} /> ), }} sx={{ // An example of how editor styles can be overridden. In this case, // setting where the scroll anchors to when jumping to headings. The // scroll margin isn't built in since it will likely vary depending on // where the editor itself is rendered (e.g. if there's a sticky nav // bar on your site). "& .ProseMirror": { "& h1, & h2, & h3, & h4, & h5, & h6": { scrollMarginTop: showMenuBar ? 50 : 0, }, }, }} > {() => ( <> )} Saved result: {submittedContent ? ( <>
            {submittedContent}
          
Read-only saved snapshot: ) : ( <> Press “Save” above to show the HTML markup for the editor content. Typically you’d use a similar editor.getHTML() approach to save your data in a form. )} ); }