--- /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>
+ )
+ }
+}