client/src/components/SlateEditor/Toolbar.js
changeset 173 0e6703cd0968
equal deleted inserted replaced
172:4b780ebbedc6 173:0e6703cd0968
       
     1 import React from 'react';
       
     2 import ToolbarButtons from './ToolbarButtons';
       
     3 import MarkButton from './MarkButton';
       
     4 import CategoryButton from './CategoryButton';
       
     5 import BlockButton from './BlockButton';
       
     6 
       
     7 /**
       
     8  * Define the default node type.
       
     9  */
       
    10 
       
    11 const DEFAULT_NODE = 'paragraph'
       
    12 
       
    13 
       
    14 /**
       
    15  * Render the toolbar.
       
    16  *
       
    17  * @return {Element}
       
    18  */
       
    19 export default class Toolbar extends React.Component {
       
    20 
       
    21   /**
       
    22    * Deserialize the initial editor state.
       
    23    *
       
    24    * @type {Object}
       
    25    */
       
    26   constructor(props) {
       
    27     super(props);
       
    28     this.editorRef = React.createRef();
       
    29   }
       
    30 
       
    31   /**
       
    32    * Check if the current selection has a mark with `type` in it.
       
    33    *
       
    34    * @param {String} type
       
    35    * @return {Boolean}
       
    36    */
       
    37 
       
    38   hasMark = type => {
       
    39     const { value } = this.props;
       
    40     return value.activeMarks.some(mark => mark.type === type);
       
    41   }
       
    42 
       
    43   /**
       
    44    * Check if the any of the currently selected blocks are of `type`.
       
    45    *
       
    46    * @param {String} type
       
    47    * @return {Boolean}
       
    48    */
       
    49 
       
    50   hasBlock = type => {
       
    51     const { value } = this.props;
       
    52     return value.blocks.some(node => node.type === type)
       
    53   }
       
    54 
       
    55 
       
    56   /**
       
    57    * When a mark button is clicked, toggle the current mark.
       
    58    *
       
    59    * @param {Event} e
       
    60    * @param {String} type
       
    61    */
       
    62 
       
    63   onClickMark = (e, type) => {
       
    64     this.props.editor.toggleMark(type)
       
    65   }
       
    66 
       
    67   isBlockActive = (type) => {
       
    68     let isActive = this.hasBlock(type)
       
    69 
       
    70     if (['numbered-list', 'bulleted-list'].includes(type)) {
       
    71       const { value } = this.props;
       
    72       const firstBlock = value.blocks.first();
       
    73       if(firstBlock) {
       
    74         const parent = value.document.getParent(firstBlock.key);
       
    75         isActive = this.hasBlock('list-item') && parent && parent.type === type;
       
    76       }
       
    77     }
       
    78 
       
    79     return isActive;
       
    80   }
       
    81 
       
    82   onClickCategoryButton = (openPortal, closePortal, isOpen, e) => {
       
    83     e.preventDefault();
       
    84     const { value, editor } = this.props;
       
    85 
       
    86     // Can't use toggleMark here, because it expects the same object
       
    87     // @see https://github.com/ianstormtaylor/slate/issues/873
       
    88     if (this.hasMark('category')) {
       
    89       value.activeMarks.filter(mark => mark.type === 'category')
       
    90         .forEach(mark => editor.removeMark(mark));
       
    91       closePortal();
       
    92     } else {
       
    93       openPortal();
       
    94     }
       
    95   }
       
    96 
       
    97   getSelectionParams = (value) => {
       
    98 
       
    99     const { selection } = value
       
   100     const { start, end} = selection
       
   101 
       
   102     if (selection.isCollapsed) {
       
   103       return {};
       
   104     }
       
   105 
       
   106     const nodes = [];
       
   107     let hasStarted = false;
       
   108     let hasEnded = false;
       
   109 
       
   110     // Keep only the relevant nodes,
       
   111     // i.e. nodes which are contained within selection
       
   112     value.document.nodes.forEach((node) => {
       
   113       if (start.isInNode(node)) {
       
   114         hasStarted = true;
       
   115       }
       
   116       if (hasStarted && !hasEnded) {
       
   117         nodes.push(node);
       
   118       }
       
   119       if (end.isAtEndOfNode(node)) {
       
   120         hasEnded = true;
       
   121       }
       
   122     });
       
   123 
       
   124     // Concatenate the nodes text
       
   125     const text = nodes.map((node) => {
       
   126       let textStart = start.isInNode(node) ? start.offset : 0;
       
   127       let textEnd = end.isInNode(node) ? end.offset : node.text.length;
       
   128       return node.text.substring(textStart,textEnd);
       
   129     }).join('\n');
       
   130 
       
   131     return {
       
   132       currentSelectionText: text,
       
   133       currentSelectionStart: start.offset,
       
   134       currentSelectionEnd: end.offset
       
   135     };
       
   136   }
       
   137 
       
   138   onCategoryClick = (closePortal, category) => {
       
   139 
       
   140     const { value, editor } = this.props;
       
   141 
       
   142     const { currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.getSelectionParams(value);
       
   143 
       
   144     if(!currentSelectionText) {
       
   145       closePortal();
       
   146       return;
       
   147     }
       
   148 
       
   149     const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category')
       
   150     categoryMarks.forEach(mark => this.editor.removeMark(mark));
       
   151 
       
   152     editor.addMark({
       
   153       type: 'category',
       
   154       data: {
       
   155         text: currentSelectionText,
       
   156         selection: {
       
   157           start: currentSelectionStart,
       
   158           end: currentSelectionEnd,
       
   159         },
       
   160         color: category.color,
       
   161         key: category.key,
       
   162         name: category.name,
       
   163         comment: category.comment
       
   164       }
       
   165     })
       
   166 
       
   167     closePortal();
       
   168   }
       
   169 
       
   170     /**
       
   171    * When a block button is clicked, toggle the block type.
       
   172    *
       
   173    * @param {Event} e
       
   174    * @param {String} type
       
   175    */
       
   176 
       
   177   onClickBlock = (e, type) => {
       
   178     e.preventDefault();
       
   179 
       
   180     const { editor, value } = this.props;
       
   181 
       
   182     // Handle everything but list buttons.
       
   183     if (type !== 'bulleted-list' && type !== 'numbered-list') {
       
   184       const isActive = this.hasBlock(type)
       
   185       const isList = this.hasBlock('list-item')
       
   186 
       
   187       if (isList) {
       
   188         editor
       
   189           .setBlocks(isActive ? DEFAULT_NODE : type)
       
   190           .unwrapBlock('bulleted-list')
       
   191           .unwrapBlock('numbered-list')
       
   192       }
       
   193 
       
   194       else {
       
   195        editor
       
   196           .setBlocks(isActive ? DEFAULT_NODE : type)
       
   197       }
       
   198     }
       
   199 
       
   200     // Handle the extra wrapping required for list buttons.
       
   201     else {
       
   202       const isList = this.hasBlock('list-item')
       
   203       const isType = value.blocks.some((block) => {
       
   204         return !!document.getClosest(block.key, parent => parent.type === type)
       
   205       })
       
   206 
       
   207       if (isList && isType) {
       
   208        editor
       
   209           .setBlocks(DEFAULT_NODE)
       
   210           .unwrapBlock('bulleted-list')
       
   211           .unwrapBlock('numbered-list')
       
   212 
       
   213       } else if (isList) {
       
   214         editor
       
   215           .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list')
       
   216           .wrapBlock(type)
       
   217 
       
   218       } else {
       
   219         editor
       
   220           .setBlocks('list-item')
       
   221           .wrapBlock(type)
       
   222 
       
   223       }
       
   224     }
       
   225   }
       
   226 
       
   227 
       
   228   render = () => {
       
   229     return (
       
   230       <div className="menu toolbar-menu d-flex sticky-top bg-secondary">
       
   231         <MarkButton icon='format_bold' isActive={this.hasMark('bold')} onMouseDown={(e) => this.onClickMark(e, 'bold')} />
       
   232         <MarkButton icon='format_italic' isActive={this.hasMark('italic')} onMouseDown={(e) => this.onClickMark(e, 'italic')} />
       
   233         <MarkButton icon='format_underlined' isActive={this.hasMark('underlined')} onMouseDown={(e) => this.onClickMark(e, 'underlined')} />
       
   234 
       
   235         <CategoryButton
       
   236           isActive={this.hasMark('category')}
       
   237           onClickCategoryButton={this.onClickCategoryButton}
       
   238           onCategoryClick={this.onCategoryClick}
       
   239           annotationCategories={this.props.annotationCategories}
       
   240         />
       
   241 
       
   242         <BlockButton icon='format_list_numbered' isActive={this.isBlockActive('numbered-list')} onMouseDown={e => this.onClickBlock(e, 'numbered-list')} />
       
   243         <BlockButton icon='format_list_bulleted' isActive={this.isBlockActive('bulleted-list')} onMouseDown={e => this.onClickBlock(e, 'bulleted-list')} />
       
   244 
       
   245         <ToolbarButtons
       
   246           hasNote={!!this.props.note}
       
   247           isButtonDisabled={this.props.isButtonDisabled}
       
   248           submitNote={this.props.submitNote}
       
   249         />
       
   250 
       
   251       </div>
       
   252     )
       
   253   }
       
   254 }