Reputation: 1069
Let's consider react application with Tiny.MCE component. I need to create a custom plugin. But I would like to create this component as a React component. I have no idea how could I inject the react component to my plugin. Any idea?
Upvotes: 2
Views: 2986
Reputation: 6232
After a LONG fight, I did it.
To start we need to understand that for the plugins to work, we need to be able to inject the project context. In my case, I was using react-intl
, Material-UI
, Axios
, react-query
, and you might have others, so I created a component for that:
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
import AxiosInterceptors from "AxiosInterceptors";
import { GlobalDataProvider } from "hooks/globalData";
import { queryClient } from "index";
import en from "intl/en";
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { IntlProvider } from "react-intl";
import { QueryClientProvider } from "react-query";
import DateFnsUtils from "@date-io/date-fns";
type AppDependenciesProviderProps = {
children: React.ReactNode;
};
export const AppDependenciesProvider = ({
children,
}: AppDependenciesProviderProps) => {
const onIntlError = (err: any) => {
console.log(err);
};
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<AxiosInterceptors />
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en" messages={en} onError={onIntlError}>
<GlobalDataProvider>
<DndProvider backend={HTML5Backend}>{children}</DndProvider>
</GlobalDataProvider>
</IntlProvider>
</QueryClientProvider>
</MuiPickersUtilsProvider>
);
};
This component is called in my App.tsx
as well in my plugin.
Then, when you call the editor, let's suppose that you need to create a custom component called medialibrary, which in my case it was used to insert images from a custom company blob and to insert images in the editor.
For the TintMCE components, I have created a wrapper one called RichTextEditor
to call it simpler from the outside, with less parameters.
import {
withStyles,
} from "@material-ui/core";
import { Editor, IAllProps } from "@tinymce/tinymce-react";
import clsx from "clsx";
import RegisterPlugins from "components/TinyMCEPlugins/Plugins/RegisterPlugins";
import { useCallback, useRef, useState } from "react";
import { FieldInputProps, FieldMetaState } from "react-final-form";
import { useIntl } from "react-intl";
import "tinymce/icons/default";
import "tinymce/plugins/code";
import "tinymce/plugins/image";
import "tinymce/plugins/imagetools";
import "tinymce/plugins/link";
import "tinymce/plugins/lists";
import "tinymce/plugins/paste";
import "tinymce/plugins/table";
import "tinymce/plugins/template";
import "tinymce/plugins/textcolor";
import "tinymce/skins/content/default/content.min.css";
import "tinymce/skins/ui/oxide/content.min.css";
import "tinymce/skins/ui/oxide/skin.min.css";
import "tinymce/themes/silver";
import "tinymce/tinymce";
import { Editor as TinyMCEEditor } from "tinymce/tinymce";
type RichTextEditorProps = {
setupEditor?: (e: TinyMCEEditor) => void;
} & Partial<IAllProps>;
const RichTextEditor = ({
setupEditor,
init = {},
...rest
}: RichTextEditorProps) => {
const [value, setValue] = useState(input.value);
const intl = useIntl();
const setup = useCallback((editor: TinyMCEEditor) => {
RegisterPlugins({ editor, intl, ...rest });
setupEditor && setupEditor(editor);
}, []);
return (
<FormControl
fullWidth
error={meta.error && meta.submitFailed}
focused={focused}
>
<InputLabel shrink={forceLabelShrink || value !== "" ? true : undefined}>
{label !== undefined ? label : ""}
</InputLabel>
<div>
<Editor
value={value}
onEditorChange={onEditorChange}
onSelectionChange={() => false}
onBlur={onBlur}
onFocus={onFocus}
init={{
branding: false,
skin: "outside",
content_css: false,
height: 200,
menubar: false,
plugins: [
"medialibrary",
"paste",
"lists",
"image",
"code",
"imagetools",
"link",
"template",
"table",
],
statusbar: false,
toolbar:
"styleselect | bold italic | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist",
table_toolbar: false,
setup,
...init,
}}
{...rest}
/>
</div>
{meta.error && meta.submitFailed && (
<FormHelperText>{meta.error}</FormHelperText>
)}
</FormControl>
);
};
export default withStyles(styles)(RichTextEditor);
Now, we need to declare the component RegisterPlugins
which allow us to declare all our React custom plugins:
import MediaLibraryPlugin from "components/TinyMCEPlugins/Plugins/MediaLibraryPlugin";
import { IntlShape } from "react-intl";
import { Editor } from "tinymce";
export type PluginRegistrationProps = {
editor: Editor;
intl?: IntlShape; /*in my case, I send the intl from the caller, but you can avoid this*/
};
const RegisterPlugins = ({ ...props }: PluginRegistrationProps) => {
/*Declare all your plugins here */
MediaLibraryPlugin({ ...props }); /*this is my custom component*/
};
export default RegisterPlugins;
At this point, we need to focus on our custom Plugin, which I recomend to divide in two steps: One for declaring the dialog wrapper and another for declaring what the component does:
import { Dialog, DialogContent } from "@material-ui/core";
import { AppDependenciesProvider } from "components/App/AppDependenciesProvider";
import { PluginRegistrationProps } from "components/TinyMCEPlugins/Plugins/RegisterPlugins";
import ReactDOM from "react-dom";
import { MediaLibraryPluginView } from "./MediaLibraryPluginView";
const MediaLibraryPlugin = ({
editor,
intl,
...rest
}: PluginRegistrationProps) => {
editor.ui.registry.addButton("medialibrary"/*name of the plugin*/, {
text: "Media Library",
icon: "gallery", /*You can pick your custom icon*/
onAction: function () {
const dialogContainer = document.createElement("div");
document.body.appendChild(dialogContainer);
const closeDialog = () => {
ReactDOM.unmountComponentAtNode(dialogContainer);
dialogContainer.remove();
};
const dialog = (
<Dialog fullWidth maxWidth={"lg"} open onClose={closeDialog}>
<DialogContent>
<AppDependenciesProvider> /*Here we inject the app context as explained in the first step*/
<MediaLibraryPluginView onClose={closeDialog} editor={editor} />
</AppDependenciesProvider>
</DialogContent>
</Dialog>
);
ReactDOM.render(dialog, dialogContainer);
},
});
};
export default MediaLibraryPlugin;
Finally, we can focus on our React code:
import useBoolean from "hooks/useBoolean";
import MediaLibraryItemCreateForm from "pages/MediaLibraryPage/MediaLibraryItemCreateForm";
import { MediaLibraryItemsListView } from "pages/MediaLibraryPage/MediaLibraryItemsListView";
import { IMediaLibraryItem } from "services/KnowfullyAdminClient";
import { TinyMCEPluginBase } from "types/common-types";
import { FileFormat } from "utils/mediaUtils";
export const MediaLibraryPluginView = ({
editor,
onClose,
}: TinyMCEPluginBase) => {
/*This component is absolutely custom. As you can see you can use hooks and whatever dependencies you need to make it work. In my case, this component inserts images coming from a custom source. Demo can be seen below*/
const [onCreate, setOnCreate] = useBoolean(false);
const onClickCreateMediaLibraryItem = () => {
setOnCreate.setTrue();
};
const onClickMediaLibraryItem = (mediaLibraryItem: IMediaLibraryItem) => {
editor.insertContent(
`<img src="${mediaLibraryItem.fileUrl}" width="${mediaLibraryItem.width}" height="${mediaLibraryItem.height}"/>`
);
onClose();
};
return !onCreate ? (
<MediaLibraryItemsListView
onClickCreateMediaLibraryItem={onClickCreateMediaLibraryItem}
onClickMediaLibraryItem={onClickMediaLibraryItem}
options={[FileFormat.IMAGE]}
contentType={FileFormat.IMAGE}
/>
) : (
<MediaLibraryItemCreateForm onRedirectBack={() => setOnCreate.setFalse()} />
);
};
Here you have the result:
Upvotes: 2
Reputation: 13746
TinyMCE plugins have a specific format for how the JavaScript has to be created/bundled. A React component won't fulfill that requirement.
The process for creating a TinyMCE plugin is documented here:
https://www.tiny.cloud/docs/advanced/creating-a-plugin/
I would recommend placing your custom plugins in their own directory and using the external_plugins
option to load them. This will keep your custom code separate from the editor and avoid things being deleted/overwritten if you update TinyMCE itself.
Upvotes: 3