client/src/components/SlateEditor/Toolbar.js
author ymh <ymh.work@gmail.com>
Fri, 16 Nov 2018 11:19:13 +0100
changeset 173 0e6703cd0968
permissions -rw-r--r--
Correct the Note editor. Split the source file in sub components. Correct a timing problem on the editor checkbox.

import React from 'react';
import ToolbarButtons from './ToolbarButtons';
import MarkButton from './MarkButton';
import CategoryButton from './CategoryButton';
import BlockButton from './BlockButton';

/**
 * Define the default node type.
 */

const DEFAULT_NODE = 'paragraph'


/**
 * Render the toolbar.
 *
 * @return {Element}
 */
export default class Toolbar extends React.Component {

  /**
   * Deserialize the initial editor state.
   *
   * @type {Object}
   */
  constructor(props) {
    super(props);
    this.editorRef = React.createRef();
  }

  /**
   * Check if the current selection has a mark with `type` in it.
   *
   * @param {String} type
   * @return {Boolean}
   */

  hasMark = type => {
    const { value } = this.props;
    return value.activeMarks.some(mark => mark.type === type);
  }

  /**
   * Check if the any of the currently selected blocks are of `type`.
   *
   * @param {String} type
   * @return {Boolean}
   */

  hasBlock = type => {
    const { value } = this.props;
    return value.blocks.some(node => node.type === type)
  }


  /**
   * When a mark button is clicked, toggle the current mark.
   *
   * @param {Event} e
   * @param {String} type
   */

  onClickMark = (e, type) => {
    this.props.editor.toggleMark(type)
  }

  isBlockActive = (type) => {
    let isActive = this.hasBlock(type)

    if (['numbered-list', 'bulleted-list'].includes(type)) {
      const { value } = this.props;
      const firstBlock = value.blocks.first();
      if(firstBlock) {
        const parent = value.document.getParent(firstBlock.key);
        isActive = this.hasBlock('list-item') && parent && parent.type === type;
      }
    }

    return isActive;
  }

  onClickCategoryButton = (openPortal, closePortal, isOpen, e) => {
    e.preventDefault();
    const { value, editor } = this.props;

    // Can't use toggleMark here, because it expects the same object
    // @see https://github.com/ianstormtaylor/slate/issues/873
    if (this.hasMark('category')) {
      value.activeMarks.filter(mark => mark.type === 'category')
        .forEach(mark => editor.removeMark(mark));
      closePortal();
    } else {
      openPortal();
    }
  }

  getSelectionParams = (value) => {

    const { selection } = value
    const { start, end} = selection

    if (selection.isCollapsed) {
      return {};
    }

    const nodes = [];
    let hasStarted = false;
    let hasEnded = false;

    // Keep only the relevant nodes,
    // i.e. nodes which are contained within selection
    value.document.nodes.forEach((node) => {
      if (start.isInNode(node)) {
        hasStarted = true;
      }
      if (hasStarted && !hasEnded) {
        nodes.push(node);
      }
      if (end.isAtEndOfNode(node)) {
        hasEnded = true;
      }
    });

    // Concatenate the nodes text
    const text = nodes.map((node) => {
      let textStart = start.isInNode(node) ? start.offset : 0;
      let textEnd = end.isInNode(node) ? end.offset : node.text.length;
      return node.text.substring(textStart,textEnd);
    }).join('\n');

    return {
      currentSelectionText: text,
      currentSelectionStart: start.offset,
      currentSelectionEnd: end.offset
    };
  }

  onCategoryClick = (closePortal, category) => {

    const { value, editor } = this.props;

    const { currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.getSelectionParams(value);

    if(!currentSelectionText) {
      closePortal();
      return;
    }

    const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category')
    categoryMarks.forEach(mark => this.editor.removeMark(mark));

    editor.addMark({
      type: 'category',
      data: {
        text: currentSelectionText,
        selection: {
          start: currentSelectionStart,
          end: currentSelectionEnd,
        },
        color: category.color,
        key: category.key,
        name: category.name,
        comment: category.comment
      }
    })

    closePortal();
  }

    /**
   * When a block button is clicked, toggle the block type.
   *
   * @param {Event} e
   * @param {String} type
   */

  onClickBlock = (e, type) => {
    e.preventDefault();

    const { editor, value } = this.props;

    // Handle everything but list buttons.
    if (type !== 'bulleted-list' && type !== 'numbered-list') {
      const isActive = this.hasBlock(type)
      const isList = this.hasBlock('list-item')

      if (isList) {
        editor
          .setBlocks(isActive ? DEFAULT_NODE : type)
          .unwrapBlock('bulleted-list')
          .unwrapBlock('numbered-list')
      }

      else {
       editor
          .setBlocks(isActive ? DEFAULT_NODE : type)
      }
    }

    // Handle the extra wrapping required for list buttons.
    else {
      const isList = this.hasBlock('list-item')
      const isType = value.blocks.some((block) => {
        return !!document.getClosest(block.key, parent => parent.type === type)
      })

      if (isList && isType) {
       editor
          .setBlocks(DEFAULT_NODE)
          .unwrapBlock('bulleted-list')
          .unwrapBlock('numbered-list')

      } else if (isList) {
        editor
          .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list')
          .wrapBlock(type)

      } else {
        editor
          .setBlocks('list-item')
          .wrapBlock(type)

      }
    }
  }


  render = () => {
    return (
      <div className="menu toolbar-menu d-flex sticky-top bg-secondary">
        <MarkButton icon='format_bold' isActive={this.hasMark('bold')} onMouseDown={(e) => this.onClickMark(e, 'bold')} />
        <MarkButton icon='format_italic' isActive={this.hasMark('italic')} onMouseDown={(e) => this.onClickMark(e, 'italic')} />
        <MarkButton icon='format_underlined' isActive={this.hasMark('underlined')} onMouseDown={(e) => this.onClickMark(e, 'underlined')} />

        <CategoryButton
          isActive={this.hasMark('category')}
          onClickCategoryButton={this.onClickCategoryButton}
          onCategoryClick={this.onCategoryClick}
          annotationCategories={this.props.annotationCategories}
        />

        <BlockButton icon='format_list_numbered' isActive={this.isBlockActive('numbered-list')} onMouseDown={e => this.onClickBlock(e, 'numbered-list')} />
        <BlockButton icon='format_list_bulleted' isActive={this.isBlockActive('bulleted-list')} onMouseDown={e => this.onClickBlock(e, 'bulleted-list')} />

        <ToolbarButtons
          hasNote={!!this.props.note}
          isButtonDisabled={this.props.isButtonDisabled}
          submitNote={this.props.submitNote}
        />

      </div>
    )
  }
}