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
-
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. -
Generate a plugin:
cd my-app yarn strapi generate
Choose “plugin” from the list, press Enter and name the plugin
wysiwyg
. -
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 }, // ... }
-
Install the required dependencies:
cd src/plugins/wysiwyg yarn add @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic
-
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 openlocalhost:1337
with a JSON format error. Creating a user onlocalhost: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: