DEV Community

Cover image for Change the default WYSIWYG to Toast UI editor
Strapi
Strapi

Posted on

Change the default WYSIWYG to Toast UI editor

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
Enter fullscreen mode Exit fullscreen mode

Perfect, all you have to do is build and launch your Strapi application:

yarn build
yarn develop
# or
npm run build
npm run develop
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

strapi-toast-ui

To learn more about other WYSIWYG

Discussion (2)

Collapse
sachinchaurasiya profile image
Sachin Chaurasiya • Edited on

Is there any way to add mention and hashtag features?

Collapse
shriji profile image
Shriji

Awesome!