我可以在tinymce编辑器中插入一个react组件吗

问题描述 投票:0回答:1

我可以在tinymce编辑器中插入react组件吗?

我需要自定义一个工具栏,用户点击它后,我需要在tinymce编辑器中插入一个react组件,我可以实现吗?

我可以在tinymce编辑器中插入react组件吗?

我需要自定义一个工具栏,用户点击它后,我需要在tinymce编辑器中插入一个react组件,我可以实现吗?

tinymce tinymce-5 tinymce-6
1个回答
0
投票

首先,我们需要了解,为了使插件正常工作,我们需要能够注入项目上下文。就我而言,我使用的是

react-intl
Material-UI
Axios
react-query
,你可能还有其他的,所以我为此创建了一个组件:

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>
  );
};

这个组件在我的

App.tsx
以及我的插件中被调用。

然后,当您调用编辑器时,假设您需要创建一个名为 medialibrary 的自定义组件(在我的例子中),它用于插入来自自定义公司 blob 的图像以及在编辑器中插入图像。我创建了一个

RichTextEditor
组件,以便从外部更简单地调用它。

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);

现在,我们需要声明组件

RegisterPlugins
,它允许我们声明所有 React 自定义插件:

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;

此时,我们需要关注我们的自定义插件,我建议将其分为两步:一个用于声明对话框包装器,另一个用于声明组件的功能:

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;

最后,我们可以专注于我们的 React 代码:

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()} />
  );
};

结果如下:

© www.soinside.com 2019 - 2024. All rights reserved.