client/src/components/SlateEditor/Toolbar.js
changeset 173 0e6703cd0968
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/SlateEditor/Toolbar.js	Fri Nov 16 11:19:13 2018 +0100
@@ -0,0 +1,254 @@
+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>
+    )
+  }
+}