client/src/components/SlateEditor/index.js
author ymh <ymh.work@gmail.com>
Tue, 29 Mar 2022 11:23:56 +0200
changeset 211 244a90638e80
parent 195 669b563563f5
permissions -rw-r--r--
Added tag 0.2.3 for changeset 3de92ddba2de

import { Value } from 'slate';
import Plain from 'slate-plain-serializer';
import { Editor } from 'slate-react';
import React from 'react';
import { withNamespaces } from 'react-i18next';
import { connect } from 'react-redux';
import HtmlSerializer from './HtmlSerializer';
import { now } from '../../utils';
import Toolbar from './Toolbar';
import { getAutoSubmit } from '../../selectors/authSelectors';


/**
 *
 * @type {Component}
 */

class SlateEditor extends React.Component {

  /**
   * Deserialize the initial editor state.
   *
   * @type {Object}
   */
  constructor(props) {
    super(props);

    this.state = {
      value: props.note ? Value.fromJSON(JSON.parse(props.note.raw)) : Plain.deserialize(''),
      startedAt: null,
      finishedAt: null,
      enterKeyValue: 0,
    };

    this.editorRef = React.createRef();
  }

  get editor() {
    if(this.editorRef) {
      return this.editorRef.current;
    }
    return null;
  }

  componentDidMount = () => {
    this.focus();
  }

   /**
   * On change, save the new state.
   *
   * @param {Change} change
   */

  onChange = ({value, operations}) => {

    const newState = {
      value
    };

    (operations || []).some((op) => {
      if(['insert_text', 'remove_text', 'add_mark', 'remove_mark', 'set_mark', 'insert_node', 'merge_node', 'move_node', 'remove_node', 'set_node', 'split_node'].indexOf(op.type)>=0) {
        const tsnow = now();
        if(this.state.startedAt == null) {
          newState.startedAt = tsnow;
        }
        newState.finishedAt = tsnow;
        return true;
      }
      return false;
    });

    this.setState(newState, () => {
      if (typeof this.props.onChange === 'function') {
        this.props.onChange(this.state, {value, operations});
      }
    })
  }


  asPlain = () => {
    return Plain.serialize(this.state.value);
  }

  asRaw = () => {
    return JSON.stringify(this.state.value.toJSON());
  }

  asHtml = () => {
    return HtmlSerializer.serialize(this.state.value);
  }

  asCategories = () => {
    return this.state.value.document.getMarksByType('category').map((mark) => mark.data.toJS()).toArray();
  }

  clear = () => {
    const value = Plain.deserialize('');
    this.setState({
      value,
      enterKeyValue: 0
    })
  }

  focus = () => {
    if(this.editor) {
      this.editor.focus();
    }
  }

  submitNote = () => {
    this.setState({ enterKeyValue: 0 }, () => {
      if (typeof this.props.submitNote === 'function') {
        this.props.submitNote();
      }
    });
  }

  /**
   * On key down, if it's a formatting command toggle a mark.
   *
   * @param {Event} e
   * @param {Change} change
   * @return {Change}
   */

  onKeyUp = (e, editor, next) => {

    const { value } = this.state;
    const noteText = value.document.text.trim();

    if(e.key === "Enter" && noteText.length !== 0) {
      this.setState({ enterKeyValue: this.state.enterKeyValue + 1 });
    } else if ( e.getModifierState() || (e.key !== "Control" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Alt") ) {
      this.setState({ enterKeyValue: 0 });
    }
    return next();
  }

  /**
   * On key down, if it's a formatting command toggle a mark.
   *
   * @param {Event} e
   * @param {Change} change
   * @return {Change}
   */

  onKeyDown = (e, editor, next) => {

    const { value, enterKeyValue } = this.state;
    const { autoSubmit } = this.props;
    const noteText = value.document.text.trim();

    // we prevent empty first lines
    if(e.key === "Enter" && noteText.length === 0) {
      e.preventDefault();
      return next();
    }

    // Enter submit the note
    if(e.key === "Enter" && ( enterKeyValue === 2 || e.ctrlKey || autoSubmit ) && noteText.length !== 0) {
      e.preventDefault();
      this.submitNote();
      return next();
    }

    if (!e.ctrlKey) {
      return next();
    }

    e.preventDefault();

    // Decide what to do based on the key code...
    switch (e.key) {
      default: {
        break;
      }
      // When "B" is pressed, add a "bold" mark to the text.
      case 'b': {
        editor.toggleMark('bold');
        break;
      }
      case 'i': {
        // When "U" is pressed, add an "italic" mark to the text.
        editor.toggleMark('italic');
        break;
      }
      case 'u': {
        // When "U" is pressed, add an "underline" mark to the text.
        editor.toggleMark('underlined');
        break;
      }
    }

    return next();

  }

  // Add a `renderMark` method to render marks.

  renderMark = (props, editor, next) => {
    const { children, mark, attributes } = props

    switch (mark.type) {
      case 'bold':
        return <strong {...attributes}>{children}</strong>
      case 'code':
        return <code {...attributes}>{children}</code>
      case 'italic':
        return <em {...attributes}>{children}</em>
      case 'underlined':
        return <ins {...attributes}>{children}</ins>
      case 'category':
        let spanStyle = {
          backgroundColor: mark.data.get('color')
        };
        return <span {...attributes} style={ spanStyle } >{children}</span>
      default:
        return next();
    }
  }

  renderNode = (props, editor, next) => {
    const { attributes, children, node } = props

    switch (node.type) {
      case 'block-quote':
        return <blockquote {...attributes}>{children}</blockquote>
      case 'bulleted-list':
        return <ul {...attributes}>{children}</ul>
      case 'heading-one':
        return <h1 {...attributes}>{children}</h1>
      case 'heading-two':
        return <h2 {...attributes}>{children}</h2>
      case 'list-item':
        return <li {...attributes}>{children}</li>
      case 'numbered-list':
        return <ol {...attributes}>{children}</ol>
      default:
        return next();
    }
  }

    /**
   * Render.
   *
   * @return {Element}
   */

  render = () => (
    <div className="bg-secondary mb-5">
      <div className="sticky-top">
        <Toolbar
          value={this.state.value}
          editor={this.editor}
          note={this.props.note}
          annotationCategories={this.props.annotationCategories}
          isButtonDisabled={this.props.isButtonDisabled}
          submitNote={this.submitNote}
        />
      </div>
      <div className="editor-slatejs p-2">
        <Editor
          ref={this.editorRef}
          spellCheck
          placeholder={this.props.t('slate_editor.placeholder')}
          value={this.state.value}
          onChange={this.onChange}
          onKeyDown={this.onKeyDown}
          onKeyUp={this.onKeyUp}
          renderMark={this.renderMark}
          renderNode = {this.renderNode}
        />
      </div>
    </div>
  );


}

/**
 * Export.
 */
function mapStateToProps(state, props) {

  const autoSubmit = getAutoSubmit(state);

  return {
    autoSubmit,
  };
}

export default withNamespaces("", {
  innerRef: (ref) => {
    if(!ref) {
      return;
    }
    const editorRef = (ref && ref.props) ? ref.props.editorRef : null;
    if(editorRef && editorRef.hasOwnProperty('current')) {
      editorRef.current = ref;
    }
  }
})(connect(mapStateToProps, null, null, { forwardRef: true })(SlateEditor));