Another new WYSIWYG editor that allows you to replace the default one on Strapi!
Well this time, I would not need to explain in detail how to replace it by yourself because it turns out that Fagbokforlaget, which is a Norwegian publishing company that publishes nonfiction works and teaching aids for instruction at various levels, already made an npm package you can directly add to your Strapi project in order to have a wonderful Toast UI editor in your admin.
This editor is great as it implements real-time previsualization of what you are writing.
This package is listed on our awesome Strapi repository but I find it important to write a blog post because many of you want to replace the default editor but do not know the existence of this Github repository.
However, I will explain the code of this package. This is exactly like we did for manually installing Quill editor.
It's very simple to get started. Head into an existing Strapi project and add the package via npm or yarn:
npm i --save strapi-plugin-wysiwyg-toastui
# or
yarn add strapi-plugin-wysiwyg-toastui
Perfect, all you have to do is build and launch your Strapi application:
yarn build
yarn develop
# or
npm run build
npm run develop
You can now edit your Rich Text content using the Toast UI editor!
Let's take a closer look at how they made it...
You can observe the package code by looking at the following path:
. /node_modules/strapi-plugin-wysiwyg-toastui/admin/src/components
We can see that 3 components have been created:
MediaLib component
import React, {useEffect, useState} from 'react';
import {useStrapi, prefixFileUrlWithBackendUrl} from 'strapi-helper-plugin';
import PropTypes from 'prop-types';
const MediaLib = ({isOpen, onChange, onToggle}) => {
const {
strapi: {
componentApi: {getComponent},
},
} = useStrapi ();
const [data, setData] = useState (null);
const [isDisplayed, setIsDisplayed] = useState (false);
useEffect (() => {
if (isOpen) {
setIsDisplayed (true);
}
}, [isOpen]);
const Component = getComponent ('media-library'). Component;
const handleInputChange = data => {
if (data) {
const {url} = data;
setData ({... data, url: prefixFileUrlWithBackendUrl (url)});
}
};
const handleClosed = () => {
if (data) {
onChange (data);
}
setData (null);
setIsDisplayed (false);
};
if (Component && isDisplayed) {
return (
<Component
allowedTypes = {['images', 'videos', 'files']}
isOpen = {isOpen}
multiple = {false}
noNavigation
onClosed = {handleClosed}
onInputMediaChange = {handleInputChange}
onToggle = {onToggle}
/>
);
}
return null;
};
MediaLib.defaultProps = {
isOpen: false,
onChange: () => {},
onToggle: () => {},
};
MediaLib.propTypes = {
isOpen: PropTypes.bool,
onChange: PropTypes.func,
onToggle: PropTypes.func,
};
export default MediaLib;
This component allows you to use the Media Library and will be called in the editor component in order to directly use images from the Media Library.
Nothing much! Let's see the second component.
WYSIWYG component
/**
*
* Wysiwyg
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { isEmpty, isFunction } from 'lodash';
import cn from 'classnames';
import { Description, ErrorMessage, Label } from '@buffetjs/styles';
import { Error } from '@buffetjs/core';
import Editor from '../TOASTUI';
import Wrapper from './Wrapper';
class Wysiwyg extends React.Component {
render() {
const {
autoFocus,
className,
deactivateErrorHighlight,
disabled,
error: inputError,
inputClassName,
inputDescription,
inputStyle,
label,
name,
onBlur: handleBlur,
onChange,
placeholder,
resetProps,
style,
tabIndex,
validations,
value,
...rest
} = this.props;
return (
<Error
inputError={inputError}
name={name}
type="text"
validations={validations}
>
{({ canCheck, onBlur, error, dispatch }) => {
const hasError = error && error !== null;
return (
<Wrapper
className={`${cn(!isEmpty(className) && className)} ${
hasError ? 'bordered' : ''
}`}
style={style}
>
<Label htmlFor={name}>{label}</Label>
<Editor
{...rest}
autoFocus={autoFocus}
className={inputClassName}
disabled={disabled}
deactivateErrorHighlight={deactivateErrorHighlight}
error={hasError}
name={name}
onBlur={isFunction(handleBlur) ? handleBlur : onBlur}
onChange={e => {
if (!canCheck) {
dispatch({
type: 'SET_CHECK',
});
}
dispatch({
type: 'SET_ERROR',
error: null,
});
onChange(e);
}}
placeholder={placeholder}
resetProps={resetProps}
style={inputStyle}
tabIndex={tabIndex}
value={value}
/>
{!hasError && inputDescription && (
<Description>{inputDescription}</Description>
)}
{hasError && <ErrorMessage>{error}</ErrorMessage>}
</Wrapper>
);
}}
</Error>
);
}
}
Wysiwyg.defaultProps = {
autoFocus: false,
className: '',
deactivateErrorHighlight: false,
didCheckErrors: false,
disabled: false,
error: null,
inputClassName: '',
inputDescription: '',
inputStyle: {},
label: '',
onBlur: false,
placeholder: '',
resetProps: false,
style: {},
tabIndex: '0',
validations: {},
value: null,
};
Wysiwyg.propTypes = {
autoFocus: PropTypes.bool,
className: PropTypes.string,
deactivateErrorHighlight: PropTypes.bool,
didCheckErrors: PropTypes.bool,
disabled: PropTypes.bool,
error: PropTypes.string,
inputClassName: PropTypes.string,
inputDescription: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
inputStyle: PropTypes.object,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
name: PropTypes.string.isRequired,
onBlur: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
resetProps: PropTypes.bool,
style: PropTypes.object,
tabIndex: PropTypes.string,
validations: PropTypes.object,
value: PropTypes.string,
};
export default Wysiwyg;
This component will wrap the Toast UI editor with a label and the errors.
You can see that the index.js file is not alone. There is also a Wrapper.js file containing some style using styled-components.
import styled from 'styled-components';
const Wrapper = styled.div`
padding-bottom: 2.8rem;
font-size: 1.3rem;
font-family: 'Lato';
label {
display: block;
margin-bottom: 1rem;
}
&.bordered {
.editorWrapper {
border-color: red;
}
}
> div + p {
width: 100%;
padding-top: 12px;
font-size: 1.2rem;
line-height: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: -9px;
}
`;
export default Wrapper;
Now, the final component which is the one for the Toast UI editor itself!
ToastUI component
import React from 'react';
import PropTypes from 'prop-types';
import '@toast-ui/editor/dist/toastui-editor.css';
import 'codemirror/lib/codemirror.css';
import { Editor } from '@toast-ui/react-editor';
import { Button } from '@buffetjs/core';
import MediaLib from '../MediaLib';
class TOIEditor extends React.Component {
editorRef = React.createRef();
constructor(props) {
super(props);
this.height = "400px";
this.initialEditType = "markdown";
this.previewStyle = "vertical";
this.state = { isOpen : false };
this.handleToggle = this.handleToggle.bind(this);
}
componentDidMount() {
const editor = this.editorRef.current.getInstance();
const toolbar = editor.getUI().getToolbar();
editor.eventManager.addEventType('insertMediaButton');
editor.eventManager.listen('insertMediaButton', () => {
this.handleToggle();
} );
toolbar.insertItem(0, {
type: 'button',
options: {
className: 'first tui-image',
event: 'insertMediaButton',
tooltip: 'Insert Media',
text: '@',
}
});
}
componentDidUpdate() {
// Bug fix, where switch button become submit type - editor bug
const elements = document.getElementsByClassName('te-switch-button');
if ( elements.length ) {
elements[0].setAttribute('type','button');
elements[1].setAttribute('type','button');
}
}
handleChange = data => {
let value = this.props.value;
let editor_instance = this.editorRef.current.getInstance();
if (data.mime.includes('image')) {
editor_instance.exec('AddImage', { 'altText': data.caption, 'imageUrl': data.url } );
}
else {
editor_instance.exec('AddLink', { 'linkText': data.name, 'url': data.url } );
}
};
handleToggle = () => this.setState({ isOpen : !this.state.isOpen });
render() {
return (
<>
<Editor
previewStyle={this.previewStyle}
height={this.height}
initialEditType={this.initialEditType}
initialValue={this.props.value}
ref={this.editorRef}
usageStatistics={false}
onChange={(event) => {
this.props.onChange({
target: {
value: this.editorRef.current.getInstance().getMarkdown(),
name: this.props.name,
type: 'textarea',
},
});
}}
toolbarItems={[
'heading',
'bold',
'italic',
'strike',
'divider',
'hr',
'quote',
'divider',
'ul',
'ol',
'task',
'indent',
'outdent',
'divider',
'table',
'link',
'divider',
'code',
'codeblock',
'divider',
]}
/>
<MediaLib onToggle={this.handleToggle} isOpen={this.state.isOpen} onChange={this.handleChange}/>
</>
);
}
}
export default TOIEditor;
As you understood, this component is the implementation of the new WYSIWYG which is simply using the Media Library.
That's it for this article which, I hope, will have introduced you to a very useful and especially great package.
Top comments (2)
Is there any way to add mention and hashtag features?
Awesome!