Creating a new WYSIWYG field in the admin panel

Creating a new WYSIWYG field in the admin panel

Important
This guide was created before Strapi introduced the Custom Fields feature. It is published here mostly for archiving purposes. The recommended way to add CKEditor to Strapi v4.4+ is to use the CKEditor 5 custom field plugin.
More information about Custom Fields can be found in the official documentation, both in the User Guide and the Developer Docs.

This guide will show how you can create a new field for the admin panel.

For this example, we will replace the default WYSIWYG with CKEditor in the Content Manager by creating a new plugin to add a new field to your application.

Setting up the plugin

  1. Create a new project:

    Create an application and prevent the server from starting automatically with the following command:

    yarn create strapi-app my-app --quickstart --no-run
    

    The --no-run flag was added as we will run additional commands to create a plugin right after the project generation.

  2. Generate a plugin:

    cd my-app
    yarn strapi generate
    

    Choose “plugin” from the list, press Enter and name the plugin wysiwyg.

  3. Enable the plugin by adding it to the plugins configurations file:

    // path: ./config/plugins.js
    
    module.exports = {
      // ...
      'wysiwyg': {
        enabled: true,
        resolve: './src/plugins/wysiwyg' // path to plugin folder
      },
      // ...
    }
    
  4. Install the required dependencies:

    cd src/plugins/wysiwyg
    yarn add @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic
    
  5. Start the application with the front-end development mode:

    # Go back to the application root folder
    cd ../../..
    yarn develop --watch-admin
    

Note
Launching the Strapi server in watch mode without creating a user account first will open localhost:1337 with a JSON format error. Creating a user on localhost:8081 prevents this alert.

We now need to create our new WYSIWYG, which will replace the default one in the Content Manager.

Creating the WYSIWYG

In this part, we will create 3 components:

  • a MediaLib component used to insert media in the editor
  • an Editor component that uses CKEditor as the WYSIWYG editor
  • a Wysiwyg component to wrap the CKEditor

The following code examples can be used to implement the logic for the 3 components:

Example of a MediaLib component used to insert media in the editor:

// path: ./src/plugins/wysiwyg/admin/src/components/MediaLib/index.js

import React from 'react';
import { prefixFileUrlWithBackendUrl, useLibrary } from '@strapi/helper-plugin';
import PropTypes from 'prop-types';

const MediaLib = ({ isOpen, onChange, onToggle }) => {
  const { components } = useLibrary();
  const MediaLibraryDialog = components['media-library'];

  const handleSelectAssets = files => {
    const formattedFiles = files.map(f => ({
      alt: f.alternativeText || f.name,
      url: prefixFileUrlWithBackendUrl(f.url),
      mime: f.mime,
    }));

    onChange(formattedFiles);
  };

  if(!isOpen) {
    return null
  };

  return(
    <MediaLibraryDialog onClose={onToggle} onSelectAssets={handleSelectAssets} />
  );
};

MediaLib.defaultProps = {
  isOpen: false,
  onChange: () => {},
  onToggle: () => {},
};

MediaLib.propTypes = {
  isOpen: PropTypes.bool,
  onChange: PropTypes.func,
  onToggle: PropTypes.func,
};

export default MediaLib;

Example of an Editor component using CKEditor as the WYSIWYG editor:

// path: ./src/plugins/wysiwyg/admin/src/components/Editor/index.js

import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { CKEditor } from '@ckeditor/ckeditor5-react';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import { Box } from '@strapi/design-system/Box';

const Wrapper = styled(Box)`
  .ck-editor__main {
    min-height: ${200 / 16}em;
    > div {
      min-height: ${200 / 16}em;
    }
    // Since Strapi resets css styles, it can be configured here (h2, h3, strong, i, ...)
  }
`;

const configuration = {
  toolbar: [
    'heading',
    '|',
    'bold',
    'italic',
    'link',
    'bulletedList',
    'numberedList',
    '|',
    'indent',
    'outdent',
    '|',
    'blockQuote',
    'insertTable',
    'mediaEmbed',
    'undo',
    'redo',
  ],
};

const Editor = ({ onChange, name, value, disabled }) => {
  return (
    <Wrapper>
      <CKEditor
        editor={ClassicEditor}
        disabled={disabled}
        config={configuration}
        data={value || ''}
        onReady={editor => editor.setData(value || '')}
        onChange={(event, editor) => {
          const data = editor.getData();
          onChange({ target: { name, value: data } });
        }}
      />
    </Wrapper>
  );
};

Editor.defaultProps = {
  value: '',
  disabled: false
};

Editor.propTypes = {
  onChange: PropTypes.func.isRequired,
  name: PropTypes.string.isRequired,
  value: PropTypes.string,
  disabled: PropTypes.bool
};

export default Editor;

Example of a Wysiwyg component wrapping CKEditor:

// path: ./src/plugins/wysiwyg/admin/src/components/Wysiwyg/index.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Stack } from '@strapi/design-system/Stack';
import { Box } from '@strapi/design-system/Box';
import { Button } from '@strapi/design-system/Button';
import { Typography } from '@strapi/design-system/Typography';
import Landscape from '@strapi/icons/Landscape';
import MediaLib from '../MediaLib';
import Editor from '../Editor';
import { useIntl } from 'react-intl';

const Wysiwyg = ({ name, onChange, value, intlLabel, disabled, error, description, required }) => {
  const { formatMessage } = useIntl();
  const [mediaLibVisible, setMediaLibVisible] = useState(false);

  const handleToggleMediaLib = () => setMediaLibVisible(prev => !prev);

  const handleChangeAssets = assets => {
    let newValue = value ? value : '';

    assets.map(asset => {
      if (asset.mime.includes('image')) {
        const imgTag = `<p><img src="${asset.url}" alt="${asset.alt}"></img></p>`;

        newValue = `${newValue}${imgTag}`
      }

      // Handle videos and other type of files by adding some code
    });

    onChange({ target: { name, value: newValue } });
    handleToggleMediaLib();
  };
  
  return (
    <>
      <Stack size={1}>
        <Box>
          <Typography variant="pi" fontWeight="bold">
            {formatMessage(intlLabel)}
          </Typography>
          {required && 
            <Typography variant="pi" fontWeight="bold" textColor="danger600">*</Typography>
          }
        </Box>
        <Button startIcon={<Landscape />} variant='secondary' fullWidth onClick={handleToggleMediaLib}>Media library</Button>
        <Editor 
          disabled={disabled} 
          name={name} 
          onChange={onChange} 
          value={value} 
        />
        {error && 
          <Typography variant="pi" textColor="danger600">
            {formatMessage({ id: error, defaultMessage: error })}
          </Typography>
        }
        {description && 
          <Typography variant="pi">
            {formatMessage(description)}
          </Typography>
        }
      </Stack>
      <MediaLib 
        isOpen={mediaLibVisible} 
        onChange={handleChangeAssets}
        onToggle={handleToggleMediaLib} 
      />
    </>
  );
};

Wysiwyg.defaultProps = {
  description: '',
  disabled: false,
  error: undefined,
  intlLabel: '',
  required: false,
  value: '',
};

Wysiwyg.propTypes = {
  description: PropTypes.shape({
    id: PropTypes.string,
    defaultMessage: PropTypes.string,
  }),
  disabled: PropTypes.bool, 
  error: PropTypes.string, 
  intlLabel: PropTypes.shape({
    id: PropTypes.string,
    defaultMessage: PropTypes.string,
  }),
  name: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  required: PropTypes.bool,
  value: PropTypes.string, 
};

export default Wysiwyg;

Registering the field

The last step is registering the wysiwyg field with the Wysiwyg component using addFields(). Replace the content of the admin/src/index.js field of the plugin with the following code:

// path: ./src/plugins/wysiwyg/admin/src/index.js

import pluginPkg from "../../package.json";
import Wysiwyg from "./components/Wysiwyg";
import pluginId from "./pluginId";

const name = pluginPkg.strapi.name;

export default {
  register(app) {
    app.addFields({ type: 'wysiwyg', Component: Wysiwyg });

    app.registerPlugin({
      id: pluginId,
      isReady: true,
      name,
    });
  },
  bootstrap() {},
};

And voilà, if you create a new collection type or single type with a rich text field you will see the implementation of CKEditor instead of the default WYSIWYG: