client/src/components/SlateEditor/index.js
author ymh <ymh.work@gmail.com>
Fri, 30 Nov 2018 10:53:15 +0100
changeset 183 f8f3af9e5c83
parent 173 0e6703cd0968
child 191 3f71ad81a5a9
permissions -rw-r--r--
Change the settings to avoid using Session authentication for rest framework as it raise exceptions in case client and backend are on the same domain On the filter, adapt to take into account new version of django_filters

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 * as R from 'ramda';
import HtmlSerializer from './HtmlSerializer';
import './SlateEditor.css';
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 oldState = R.clone(this.state);

    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(R.clone(this.state), oldState, {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 wrappedRef = ref.getWrappedInstance();
    const editorRef = (wrappedRef && wrappedRef.props) ? wrappedRef.props.editorRef : null;
    if(editorRef && editorRef.hasOwnProperty('current')) {
      editorRef.current = wrappedRef;
    }
  }
})(connect(mapStateToProps, null, null, { withRef: true })(SlateEditor));