client/src/components/SlateEditor.js
changeset 21 284e866f55c7
parent 19 f1b125b95fe9
child 25 e04714a1d4eb
equal deleted inserted replaced
20:a8300ef1876e 21:284e866f55c7
     1 import { Editor, Plain, Raw } from 'slate'
     1 import { Editor, Plain, Raw } from 'slate'
     2 import React from 'react'
     2 import React from 'react'
     3 import moment from 'moment';
     3 import Portal from 'react-portal'
       
     4 import moment from 'moment'
     4 import HtmlSerializer from '../HtmlSerializer'
     5 import HtmlSerializer from '../HtmlSerializer'
     5 import AnnotationPlugin from '../AnnotationPlugin';
     6 import AnnotationPlugin from '../AnnotationPlugin'
       
     7 import CategoriesTooltip from './CategoriesTooltip'
     6 
     8 
     7 const plugins = [];
     9 const plugins = [];
     8 
    10 
     9 /**
    11 /**
    10  * Define the default node type.
    12  * Define the default node type.
    15 /**
    17 /**
    16  * Define a schema.
    18  * Define a schema.
    17  *
    19  *
    18  * @type {Object}
    20  * @type {Object}
    19  */
    21  */
    20 
    22 // TODO Check if we can move this to the plugin using the schema option
       
    23 // https://docs.slatejs.org/reference/plugins/plugin.html#schema
    21 const schema = {
    24 const schema = {
    22   nodes: {
    25   nodes: {
    23     'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
    26     'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
    24     'list-item': props => <li {...props.attributes}>{props.children}</li>,
    27     'list-item': props => <li {...props.attributes}>{props.children}</li>,
    25     'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
    28     'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
    26   },
    29   },
    27   marks: {
    30   marks: {
    28     bold: {
    31     bold: {
    29       fontWeight: 'bold'
    32       fontWeight: 'bold'
    30     },
    33     },
    31     // TODO Check if we can move this to the plugin using the schema option
    34     // This is a "temporary" mark added when the hovering menu is open
    32     // https://docs.slatejs.org/reference/plugins/plugin.html#schema
    35     highlight: {
    33     annotation: {
       
    34       textDecoration: 'underline',
    36       textDecoration: 'underline',
    35       textDecorationStyle: 'dotted',
    37       textDecorationStyle: 'dotted',
    36       backgroundColor: 'yellow',
    38       backgroundColor: '#ccc',
       
    39     },
       
    40     // This is the mark actually used for annotations
       
    41     annotation: props => {
       
    42       const data = props.mark.data;
       
    43       return <span style={{ backgroundColor: data.get('color') }}>{props.children}</span>
    37     },
    44     },
    38     italic: {
    45     italic: {
    39       fontStyle: 'italic'
    46       fontStyle: 'italic'
    40     },
    47     },
    41     underlined: {
    48     underlined: {
    42       textDecoration: 'underline'
    49       textDecoration: 'underline'
    43     }
    50     }
    44   }
    51   }
    45 }
    52 }
    46 
    53 
       
    54 const annotationCategories = [
       
    55   { key: 'important', name: 'Important',    color: '#F1C40F' },
       
    56   { key: 'keyword',   name: 'Mot-clé',      color: '#2ECC71' },
       
    57   { key: 'comment',   name: 'Commentaire',  color: '#3498DB' }
       
    58 ];
       
    59 
    47 /**
    60 /**
    48  * The rich text example.
    61  * The rich text example.
    49  *
    62  *
    50  * @type {Component}
    63  * @type {Component}
    51  */
    64  */
    70 
    83 
    71     this.state = {
    84     this.state = {
    72       state: Plain.deserialize(''),
    85       state: Plain.deserialize(''),
    73       startedAt: null,
    86       startedAt: null,
    74       finishedAt: null,
    87       finishedAt: null,
    75       currentSelectionText: ''
    88       currentSelectionText: '',
       
    89       hoveringMenu: null,
       
    90       isPortalOpen: false
    76     };
    91     };
    77   }
    92   }
    78 
    93 
    79   componentDidMount() {
    94   componentDidMount = () => {
       
    95     this.updateMenu();
    80     this.focus();
    96     this.focus();
       
    97   }
       
    98 
       
    99   componentDidUpdate = () => {
       
   100     this.updateMenu();
    81   }
   101   }
    82 
   102 
    83   /**
   103   /**
    84    * Check if the current selection has a mark with `type` in it.
   104    * Check if the current selection has a mark with `type` in it.
    85    *
   105    *
   205    * @param {String} type
   225    * @param {String} type
   206    */
   226    */
   207 
   227 
   208   onClickMark = (e, type) => {
   228   onClickMark = (e, type) => {
   209     e.preventDefault()
   229     e.preventDefault()
   210     let { state } = this.state
   230     let { state, hoveringMenu } = this.state
   211 
   231 
   212     let toggleMarkOptions;
   232     let toggleMarkOptions;
   213     if (type === 'annotation') {
   233     let isPortalOpen = false;
       
   234 
       
   235     if (type === 'highlight') {
   214       toggleMarkOptions = { type: type, data: { text: this.state.currentSelectionText } }
   236       toggleMarkOptions = { type: type, data: { text: this.state.currentSelectionText } }
       
   237       isPortalOpen = !this.state.isPortalOpen;
   215     } else {
   238     } else {
   216       toggleMarkOptions = type;
   239       toggleMarkOptions = type;
   217     }
   240     }
   218 
   241 
   219     state = state
   242     state = state
   220       .transform()
   243       .transform()
   221       .toggleMark(toggleMarkOptions)
   244       .toggleMark(toggleMarkOptions)
   222       .apply()
   245       .apply()
   223 
   246 
   224     this.setState({ state })
   247     this.setState({
       
   248       state: state,
       
   249       isPortalOpen: isPortalOpen
       
   250     })
   225   }
   251   }
   226 
   252 
   227   /**
   253   /**
   228    * When a block button is clicked, toggle the block type.
   254    * When a block button is clicked, toggle the block type.
   229    *
   255    *
   280 
   306 
   281     state = transform.apply()
   307     state = transform.apply()
   282     this.setState({ state })
   308     this.setState({ state })
   283   }
   309   }
   284 
   310 
       
   311   onPortalOpen = (portal) => {
       
   312     // When the portal opens, cache the menu element.
       
   313     this.setState({ hoveringMenu: portal.firstChild })
       
   314   }
       
   315 
       
   316   onPortalClose = (portal) => {
       
   317 
       
   318     let { state } = this.state
       
   319     const transform = state.transform();
       
   320 
       
   321     state.marks.forEach(mark => {
       
   322       if (mark.type === 'highlight') {
       
   323         transform.removeMark(mark)
       
   324       }
       
   325     });
       
   326 
       
   327     this.setState({
       
   328       state: transform.apply(),
       
   329       isPortalOpen: false
       
   330     })
       
   331   }
       
   332 
       
   333   onCategoryClick = (category) => {
       
   334 
       
   335     const { state } = this.state;
       
   336     const transform = state.transform();
       
   337 
       
   338     state.marks.forEach(mark => transform.removeMark(mark));
       
   339 
       
   340     transform.addMark({
       
   341       type: 'annotation',
       
   342       data: {
       
   343         text: this.state.currentSelectionText,
       
   344         color: category.color,
       
   345         key: category.key
       
   346       }
       
   347     })
       
   348 
       
   349     this.setState({
       
   350       state: transform.apply(),
       
   351       isPortalOpen: false
       
   352     });
       
   353 
       
   354   }
       
   355 
   285   /**
   356   /**
   286    * Render.
   357    * Render.
   287    *
   358    *
   288    * @return {Element}
   359    * @return {Element}
   289    */
   360    */
   307     return (
   378     return (
   308       <div className="menu toolbar-menu">
   379       <div className="menu toolbar-menu">
   309         {this.renderMarkButton('bold', 'format_bold')}
   380         {this.renderMarkButton('bold', 'format_bold')}
   310         {this.renderMarkButton('italic', 'format_italic')}
   381         {this.renderMarkButton('italic', 'format_italic')}
   311         {this.renderMarkButton('underlined', 'format_underlined')}
   382         {this.renderMarkButton('underlined', 'format_underlined')}
   312         {this.renderMarkButton('annotation', 'label')}
   383         {this.renderMarkButton('highlight', 'label')}
   313 
   384 
   314         {this.renderBlockButton('numbered-list', 'format_list_numbered')}
   385         {this.renderBlockButton('numbered-list', 'format_list_numbered')}
   315         {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
   386         {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
   316       </div>
   387       </div>
   317     )
   388     )
   362    */
   433    */
   363 
   434 
   364   renderEditor = () => {
   435   renderEditor = () => {
   365     return (
   436     return (
   366       <div className="editor">
   437       <div className="editor">
       
   438         {this.renderHoveringMenu()}
   367         <Editor
   439         <Editor
   368           ref="editor"
   440           ref="editor"
   369           spellCheck
   441           spellCheck
   370           placeholder={'Enter some rich text...'}
   442           placeholder={'Enter some rich text...'}
   371           schema={schema}
   443           schema={schema}
   376         />
   448         />
   377       </div>
   449       </div>
   378     )
   450     )
   379   }
   451   }
   380 
   452 
       
   453   renderHoveringMenu = () => {
       
   454     return (
       
   455       <Portal ref="portal"
       
   456         isOpened={this.state.isPortalOpen} isOpen={this.state.isPortalOpen}
       
   457         onOpen={this.onPortalOpen}
       
   458         onClose={this.onPortalClose}
       
   459         closeOnOutsideClick={false} closeOnEsc={true}>
       
   460         <div className="hovering-menu">
       
   461           <CategoriesTooltip categories={annotationCategories} onCategoryClick={this.onCategoryClick} />
       
   462         </div>
       
   463       </Portal>
       
   464     )
       
   465   }
       
   466 
       
   467   updateMenu = () => {
       
   468 
       
   469     const { hoveringMenu, state } = this.state
       
   470 
       
   471     if (!hoveringMenu) return
       
   472 
       
   473     // if (state.isBlurred || state.isCollapsed) {
       
   474     //   hoveringMenu.removeAttribute('style')
       
   475     //   return
       
   476     // }
       
   477 
       
   478     const selection = window.getSelection()
       
   479 
       
   480     if (selection.isCollapsed) {
       
   481       return
       
   482     }
       
   483 
       
   484     const range = selection.getRangeAt(0)
       
   485     const rect = range.getBoundingClientRect()
       
   486 
       
   487     hoveringMenu.style.opacity = 1
       
   488     hoveringMenu.style.top = `${rect.top + window.scrollY + hoveringMenu.offsetHeight}px`
       
   489     hoveringMenu.style.left = `${rect.left + window.scrollX - hoveringMenu.offsetWidth / 2 + rect.width / 2}px`
       
   490   }
       
   491 
   381 }
   492 }
   382 
   493 
   383 /**
   494 /**
   384  * Export.
   495  * Export.
   385  */
   496  */