mui-tiptap
mui-tiptap copied to clipboard
Add more built-in features for "Insert image" menu button, for @tiptap/extension-image
Right now, there's a general-purpose "insert image" button via MenuButtonAddImage
. You provide your own onClick
(where you could use your own interface to type in a URL, or you could trigger a file upload), like in the simple example in the internal demo.
As originally mentioned/requested in https://github.com/sjdemartini/mui-tiptap/issues/52, we might also want to add support for more functionality/UI built into mui-tiptap, with:
- [ ] An interface for a user to provide a URL
- [x] A user to upload an image from their filesystem (https://github.com/sjdemartini/mui-tiptap/pull/147, available as of mui-tiptap v1.8.0)
See example screenshots here https://github.com/sjdemartini/mui-tiptap/issues/52#issuecomment-1637220903
👋🏻 Do you have a simple example of triggering a file upload when clicking the Insert image menu button?
Hi @devth, I don't have one off hand, though I plan to add one soon. I think an approach like this https://medium.com/web-dev-survey-from-kyoto/how-to-customize-the-file-upload-button-in-react-b3866a5973d8 (https://codesandbox.io/embed/how-to-customize-file-upload-buttons-in-react-wthkrz?codemirror=1) is similar to what I had in mind, except swapping the <button>
for the MenuButtonAddImage
, and using logic similar to the mui-tiptap URL example to insert the image into the editor after you upload it (using a URL returned from the server for the uploaded image). There's more to do to handle multi-file uploads/insertions with the Tiptap API, which I plan to also demo, but maybe that's enough to get you started in the meantime. I'll keep you posted when I add something.
@sjdemartini awesome, thanks so much! I’ll play around and see what I can come up with.
@devth I've released a new version 1.8.0 which includes a new component MenuButtonImageUpload
that encapsulates the image upload functionality intended/discussed above. You just need to supply your own onUploadFiles
prop in order to take/upload the image files the user provided, and return URLs at which the images can be served/viewed. The component will handle the hidden file input, button behavior, and insertion of the images into the editor content.
I've also updated the demo to show how to use drag-and-drop and pasting of image files (notes on this are in the README here).
Hope that's useful!
@sjdemartini this looks awesome! thanks.
hi i am new to programing as a whole and am trying to use the mui titap pludin in my react MUI project, i copied the all-in-one component from the gitgub repo, and am trying to add controls but i get such errors "React Router caught the following error during render TypeError: editor.can(...).toggleSuperscript is not a function", toggleSuperscript is the control i want to add for example, and this goes for all other controls i add,
as you can see from the screenshot all the commented controls give the error in the console and my application breaks. please how can i go about this.
@rextambua This comment doesn't seem to be related to the original issue here—please create new issues rather than changing the topic if you have a problem. In this case though, it appears your issue is that you haven't installed and added the necessary extensions (notably Superscript
here). See past issues about this here https://github.com/sjdemartini/mui-tiptap/issues/174#issuecomment-1783299136 (or similarly here https://github.com/sjdemartini/mui-tiptap/issues/187) and this section of the README https://github.com/sjdemartini/mui-tiptap?tab=readme-ov-file#choosing-your-editor-extensions.
sorry and thanks, i will read this material and if need arises i will create a new issue
Hey @sjdemartini, quick question on this - when I use insertImages
it correctly creates html like this in the editor:
<div class="react-renderer node-image ProseMirror-selectednode" contenteditable="false" draggable="true">
<div data-node-view-wrapper="" style="white-space: normal; text-align: right; width: 100%;">
<div class="css-1fk4pfw-ResizableImageComponent-imageContainer">
<img src="https://firebasestorage.googleapis.com/v0/b/converge-mt.appspot.com/o/images%2Fuploads%2Facme%2Feditor%2FlxsKFckgYGPnfZImBWmJKQSp3cR2%2F1712845173502.IMG_5320.jpeg?alt=media&token=c174d23e-05ab-4b38-aa29-562f3e180e7f" height="auto" width="138" class="ProseMirror-selectednode css-1cxz5lg-ResizableImageComponent-image-ResizableImageComponent-imageSelected" data-drag-handle="true" style="aspect-ratio: 1 / 1;" />
<div class="css-e5u3zl-ResizableImageResizer-root-ResizableImageComponent-resizer"></div>
</div>
</div>
</div>
But when I call editor.getHTML()
later it results in a very stripped-down equivalent:
<img height="auto" src="https://firebasestorage.googleapis.com/v0/b/converge-mt.appspot.com/o/images%2Fuploads%2Facme%2Feditor%2FlxsKFckgYGPnfZImBWmJKQSp3cR2%2F1712845173502.IMG_5320.jpeg?alt=media&token=c174d23e-05ab-4b38-aa29-562f3e180e7f" width="138" style="text-align: right; aspect-ratio: 1 / 1;" />
lacking the wrapper, which causes alignment to not work.
I compared this to the official demo and that does not happen - the resulting HTML still contains a wrapper like:
<div class="react-renderer node-image" contenteditable="false" draggable="true">
<div data-node-view-wrapper="" style="white-space: normal; text-align: right; width: 100%;">
<div class="css-1fk4pfw-ResizableImageComponent-imageContainer">
<img src="blob:https://3zl2l6-5173.csb.app/58bd9814-3f86-439d-bcfe-f215abd3fe60" height="auto" width="394" alt="snoop Samurai.jpeg" class="css-12r3bmv-ResizableImageComponent-image" data-drag-handle="true" style="aspect-ratio: 1 / 1;" />
</div>
</div>
</div>
I am configuring ResizableImage
in my useExtensions
, just like the demo. Not sure what else I'm missing 🤔
Edit: I just noticed it uses RichTextReadOnly
to render the resulting HTML - does that do some magic? Up till now I was just using html-react-parser to render the resulting html.
@devth getHTML()
does not return the full Tiptap-rendered markup (such as the react-renderer elements, etc.). getHTML
is for returning a serialized version of the the editor content—it will always use the renderHTML
method representation of each extension/node and not custom Node views (React or otherwise) or interactive elements. When its output is parsed again by Tiptap later, it should ideally produce/render the same visual result as before. But on its own, the HTML may not appear the same.
I'm not sure where you are referring to in saying that the CodeSandbox demo has getHTML
containing a wrapper. I have just tested and confirmed (via the "Save" button example there) that it only returns the ordinary img
markup and no more:
In general, you should use Tiptap to render and display Tiptap content, like with RichTextReadOnly
as you note, rather than trying to inject the HTML directly into the page.
Thanks for the explanation!
What I meant in the CodeSandbox demo was that when the html is rendered via RichTextReadOnly
it contains the wrapper when I inspect it in devtools:
I'll look into using RichTextReadOnly
in my app. 👍🏻
Not sure how I missed that after using mui-tiptap
for the last year 😅
One thing I'm trying to figure out is how I can replace anchor tags coming from tiptap getHTML
with Next.js Links. Currently I do that with html-react-parser's replace
function:
const parseHtmlWithNextLink = (html: string) => {
return htmlParse(html, {
replace: (domNode) => {
if (domNode instanceof Element && domNode.attribs) {
if (domNode.name === "a") {
const props = attributesToProps(domNode.attribs);
return (
<Link legacyBehavior href={domNode.attribs.href} {...props}>
<a {...props}>{domToReact(domNode.children as DOMNode[])}</a>
</Link>
);
}
}
},
});
};
🤔
An alternate solution I tried is to keep using html-react-parser
, and add my own wrapping logic so that image alignment works:
const parseHtmlWithNextLink = (html: string) => {
return htmlParse(html, {
replace: (domNode) => {
if (domNode instanceof Element && domNode.attribs) {
// surround images with a div that respects alignment
const props = attributesToProps(domNode.attribs);
if (domNode.name === "img") {
const style = props.style;
const divStyle = {
width: "100%",
textAlign: style?.textAlign as CSSProperties["textAlign"],
};
return (
<div style={divStyle}>
<img
alt={props.alt as string}
{...attributesToProps(domNode.attribs)}
/>
</div>
);
}
// turn anchor tags into Next.js Link elements for client-side routing
if (domNode.name === "a") {
return (
<Link legacyBehavior href={domNode.attribs.href} {...props}>
<a {...props}>{domToReact(domNode.children as DOMNode[])}</a>
</Link>
);
}
}
},
});
};
Not sure this is a good idea, but it "works".
Great work firstly . Thanks. An interface for a user to provide a URL -> Assuming this is still open ? Couldnt find a way to do this yet in the latest release. May i ask when you would condier adding this feature where just image URL is provided for render ?
Oops Ive now included a MUI diag box for insert Images. However upon inserting a Transparent PNG the background showa white. Any think I may be missing ?
@sjdemartini would be great if you could help let me know if there is something missing WRT adding transparent images
@pymenow There is not a specific built-in way to add an image from a URL in mui-tiptap, but you can do it yourself with your own mui-tiptap MenuButton, MUI Dialog, etc. as you seem to be suggesting.
In terms of transparent images, mui-tiptap currently has behavior similar to Chrome, which is to add a light grey background to all (transparent) images, as this tends to be useful since most images are created on a light background and will help make them readable even in dark mode. (Though of course some images, likely a minority of them, will have the opposite problem.) See the notes here for more of the behavior details and rationale https://github.com/sjdemartini/mui-tiptap/blob/9e4bbb5ab66294ce654ba2e7c141d21fe1e72325/src/styles.ts#L489-L516
You can override the styles by targeting img:not(.ProseMirror-separator)
inside of the mui-tiptap RichTextEditor/ProseMirror context and adding CSS to change to backgroundColor: "transparent"
or similar.