client/src/components/SlateEditor.js
changeset 159 a4705c2b4544
parent 157 5c3af4f10e92
child 161 a642639dbc07
equal deleted inserted replaced
158:964438ef8401 159:a4705c2b4544
     5 import Portal from 'react-portal'
     5 import Portal from 'react-portal'
     6 import Immutable from 'immutable'
     6 import Immutable from 'immutable'
     7 import HtmlSerializer from '../HtmlSerializer'
     7 import HtmlSerializer from '../HtmlSerializer'
     8 import AnnotationPlugin from '../AnnotationPlugin'
     8 import AnnotationPlugin from '../AnnotationPlugin'
     9 import CategoriesTooltip from './CategoriesTooltip'
     9 import CategoriesTooltip from './CategoriesTooltip'
       
    10 // import './SlateEditor.css';
    10 import { now } from '../utils';
    11 import { now } from '../utils';
    11 import { defaultAnnotationsCategories } from '../constants';
    12 import { defaultAnnotationsCategories } from '../constants';
    12 
    13 
    13 const plugins = [];
    14 const plugins = [];
    14 
    15 
    24  * @type {Object}
    25  * @type {Object}
    25  */
    26  */
    26 // TODO Check if we can move this to the plugin using the schema option
    27 // TODO Check if we can move this to the plugin using the schema option
    27 // https://docs.slatejs.org/reference/plugins/plugin.html#schema
    28 // https://docs.slatejs.org/reference/plugins/plugin.html#schema
    28 const schema = {
    29 const schema = {
       
    30 
    29   nodes: {
    31   nodes: {
    30     'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
    32     'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
    31     'list-item': props => <li {...props.attributes}>{props.children}</li>,
    33     'list-item': props => <li {...props.attributes}>{props.children}</li>,
    32     'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
    34     'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
    33   },
    35   },
    44     },
    46     },
    45     underlined: {
    47     underlined: {
    46       textDecoration: 'underlined'
    48       textDecoration: 'underlined'
    47     }
    49     }
    48   }
    50   }
       
    51 
    49 }
    52 }
    50 
    53 
    51 const initialValue = Value.fromJSON({
    54 const initialValue = Value.fromJSON({
    52   document: {
    55   document: {
    53     nodes: [
    56     nodes: [
    88     const annotationPlugin = AnnotationPlugin({
    91     const annotationPlugin = AnnotationPlugin({
    89       onChange: (text, start, end) => {
    92       onChange: (text, start, end) => {
    90         this.setState({
    93         this.setState({
    91           currentSelectionText: text,
    94           currentSelectionText: text,
    92           currentSelectionStart: start,
    95           currentSelectionStart: start,
    93           currentSelectionEnd: end
    96           currentSelectionEnd: end,
    94         });
    97         });
    95       }
    98       }
    96     });
    99     });
    97 
   100 
    98     plugins.push(annotationPlugin);
   101     plugins.push(annotationPlugin);
    99 
   102 
   100 
   103 
   101     this.state = {
   104     this.state = {
   102       value: props.note ? initialValue : Plain.deserialize(''),
   105       value: props.note ? Value.fromJSON(initialValue) : Plain.deserialize(''),
   103       startedAt: null,
   106       startedAt: null,
   104       finishedAt: null,
   107       finishedAt: null,
   105       currentSelectionText: '',
   108       currentSelectionText: '',
   106       currentSelectionStart: 0,
   109       currentSelectionStart: 0,
   107       currentSelectionEnd: 0,
   110       currentSelectionEnd: 0,
   108       hoveringMenu: null,
   111       hoveringMenu: null,
   109       isPortalOpen: false,
   112       isPortalOpen: false,
   110       categories: Immutable.List([]),
   113       categories: Immutable.List([]),
   111       isCheckboxChecked: false,
   114       isCheckboxChecked: false,
       
   115       enterKeyValue: 0,
   112     };
   116     };
   113   }
   117   }
   114 
   118 
   115   componentDidMount = () => {
   119   componentDidMount = () => {
   116     this.updateMenu();
   120     this.updateMenu();
   210 
   214 
   211   focus = () => {
   215   focus = () => {
   212     this.refs.editor.focus();
   216     this.refs.editor.focus();
   213   }
   217   }
   214 
   218 
   215   /**
       
   216    * On key down, if it's a formatting command toggle a mark.
       
   217    *
       
   218    * @param {Event} e
       
   219    * @param {Object} data
       
   220    * @param {Change} change
       
   221    * @return {Change}
       
   222    */
       
   223 
       
   224   onKeyDown = (e, change) => {
       
   225     //   if (data.key === 'enter' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') {
       
   226 
       
   227     //   e.preventDefault();
       
   228     //   this.props.onEnterKeyDown();
       
   229 
       
   230     //   return change;
       
   231     // }
       
   232 
       
   233     // if (!data.isMod) return
       
   234     if (!e.ctrlKey) return
       
   235         // Decide what to do based on the key code...
       
   236         switch (e.key) {
       
   237           // When "B" is pressed, add a "bold" mark to the text.
       
   238           case 'b': {
       
   239             e.preventDefault()
       
   240             change.toggleMark('bold')
       
   241 
       
   242             return true
       
   243           }
       
   244           case 'i': {
       
   245             // When "U" is pressed, add an "italic" mark to the text.
       
   246             e.preventDefault()
       
   247             change.toggleMark('italic')
       
   248 
       
   249             return true
       
   250           }
       
   251           case 'u': {
       
   252             // When "U" is pressed, add an "underline" mark to the text.
       
   253             e.preventDefault()
       
   254             change.toggleMark('underlined')
       
   255 
       
   256             return true
       
   257           }
       
   258         }
       
   259     }
       
   260 
       
   261       /**
   219       /**
   262    * When a mark button is clicked, toggle the current mark.
   220    * When a mark button is clicked, toggle the current mark.
   263    *
   221    *
   264    * @param {Event} e
   222    * @param {Event} e
   265    * @param {String} type
   223    * @param {String} type
   266    */
   224    */
   267 
   225 
   268   onClickMark = (e, type) => {
   226   onClickMark = (e, type) => {
   269 
   227 
   270       e.preventDefault()
   228     e.preventDefault()
   271     const { value } = this.state
   229     const { value } = this.state
   272     let { categories } = this.state
   230     let { categories } = this.state
   273 
   231 
   274     let isPortalOpen = false;
   232     let isPortalOpen = false;
   275 
   233 
   396         },
   354         },
   397         color: category.color,
   355         color: category.color,
   398         key: category.key
   356         key: category.key
   399       }
   357       }
   400     })
   358     })
   401     this.onChange(change)
       
   402 
   359 
   403     Object.assign(category, {
   360     Object.assign(category, {
   404       text: currentSelectionText,
   361       text: currentSelectionText,
   405       selection: {
   362       selection: {
   406         start: currentSelectionStart,
   363         start: currentSelectionStart,
   407         end: currentSelectionEnd,
   364         end: currentSelectionEnd,
   408       },
   365       },
   409     });
   366     });
   410     categories = categories.push(category);
   367     categories = categories.push(category);
   411 
   368 
       
   369     this.onChange(change)
       
   370 
   412     this.setState({
   371     this.setState({
   413       value: value,
       
   414       isPortalOpen: false,
   372       isPortalOpen: false,
   415       categories: categories
   373       categories: categories
   416     });
   374     });
   417   }
   375   }
   418 
   376 
   424 
   382 
   425   onCheckboxChange = (e) => {
   383   onCheckboxChange = (e) => {
   426     if (typeof this.props.onCheckboxChange === 'function') {
   384     if (typeof this.props.onCheckboxChange === 'function') {
   427       this.props.onCheckboxChange(e);
   385       this.props.onCheckboxChange(e);
   428     }
   386     }
       
   387   }
       
   388 
       
   389   /**
       
   390    * On key down, if it's a formatting command toggle a mark.
       
   391    *
       
   392    * @param {Event} e
       
   393    * @param {Change} change
       
   394    * @return {Change}
       
   395    */
       
   396 
       
   397   onKeyDown = (e, change) => {
       
   398 
       
   399     const {value} = this.state;
       
   400 
       
   401     // if (e.key === 'Enter' && value.document.text === '') {
       
   402     //   change.removeChild()
       
   403     // }
       
   404 
       
   405     if (e.key === 'Enter' && value.document.text !== '') {
       
   406       this.setState({enterKeyValue: 1})
       
   407     }
       
   408 
       
   409     if (e.key !== 'Enter') {
       
   410       this.setState({
       
   411         enterKeyValue: 0,
       
   412       })
       
   413 
       
   414     }
       
   415 
       
   416     if (e.key === 'Enter' && !this.props.isChecked && this.state.enterKeyValue === 1 && typeof this.props.onEnterKeyDown === 'function') {
       
   417       e.preventDefault();
       
   418       this.props.onEnterKeyDown();
       
   419       this.setState({
       
   420         enterKeyValue: 0,
       
   421       })
       
   422 
       
   423 
       
   424       return change
       
   425     }
       
   426 
       
   427     else if (e.key === 'Enter' && value.document.text !== '' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') {
       
   428 
       
   429       e.preventDefault();
       
   430       this.props.onEnterKeyDown();
       
   431 
       
   432       return change
       
   433     }
       
   434 
       
   435     if (!e.ctrlKey) return
       
   436         // Decide what to do based on the key code...
       
   437         switch (e.key) {
       
   438           default: {
       
   439             break;
       
   440           }
       
   441           // When "B" is pressed, add a "bold" mark to the text.
       
   442           case 'b': {
       
   443             e.preventDefault()
       
   444             change.toggleMark('bold')
       
   445 
       
   446             return true
       
   447           }
       
   448           case 'i': {
       
   449             // When "U" is pressed, add an "italic" mark to the text.
       
   450             e.preventDefault()
       
   451             change.toggleMark('italic')
       
   452 
       
   453             return true
       
   454           }
       
   455           case 'u': {
       
   456             // When "U" is pressed, add an "underline" mark to the text.
       
   457             e.preventDefault()
       
   458             change.toggleMark('underlined')
       
   459 
       
   460             return true
       
   461           }
       
   462           case 'Enter': {
       
   463             // When "ENTER" is pressed, autosubmit the note.
       
   464             if (value.document.text !== '' && typeof this.props.onEnterKeyDown === 'function') {
       
   465               e.preventDefault()
       
   466               this.props.onEnterKeyDown();
       
   467               this.setState({
       
   468                 enterKeyValue: 0,
       
   469               })
       
   470 
       
   471               return true
       
   472             }
       
   473         }
       
   474       }
   429   }
   475   }
   430 
   476 
   431   /**
   477   /**
   432    * Render.
   478    * Render.
   433    *
   479    *
   451    * @return {Element}
   497    * @return {Element}
   452    */
   498    */
   453 
   499 
   454   renderToolbar = () => {
   500   renderToolbar = () => {
   455     return (
   501     return (
   456       <div className="menu toolbar-menu d-flex bg-secondary">
   502       <div className="menu toolbar-menu d-flex sticky-top bg-secondary">
   457           {this.renderMarkButton('bold', 'format_bold')}
   503           {this.renderMarkButton('bold', 'format_bold')}
   458           {this.renderMarkButton('italic', 'format_italic')}
   504           {this.renderMarkButton('italic', 'format_italic')}
   459           {this.renderMarkButton('underlined', 'format_underlined')}
   505           {this.renderMarkButton('underlined', 'format_underlined')}
   460           {this.renderMarkButton('category', 'label')}
   506           {this.renderMarkButton('category', 'label')}
   461 
   507 
   476         </label>
   522         </label>
   477       </div>
   523       </div>
   478     )
   524     )
   479   }
   525   }
   480 
   526 
       
   527   renderSaveButton = () => {
       
   528     if (this.props.note) {
       
   529       return <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold mr-2" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
       
   530       Sauvegarder</button>
       
   531     }
       
   532   }
       
   533 
   481   renderToolbarButtons = () => {
   534   renderToolbarButtons = () => {
   482     return (
   535     return (
   483       <div>
   536       <div>
   484         <button type="button" className="btn btn-primary btn-sm text-secondary float-right mr-5" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
   537         {/* <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold float-right" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}> */}
   485           { this.props.note ? 'Save note' : 'Ajouter' }
   538           {/* { this.props.note ? 'Sauvegarder' : 'Ajouter' } */}
   486         </button>
   539           {this.renderSaveButton()}
       
   540         {/* </button> */}
   487         { !this.props.note && this.renderToolbarCheckbox() }
   541         { !this.props.note && this.renderToolbarCheckbox() }
   488       </div>
   542       </div>
   489     );
   543     );
   490   }
   544   }
   491 
   545 
   498    */
   552    */
   499 
   553 
   500   renderMarkButton = (type, icon) => {
   554   renderMarkButton = (type, icon) => {
   501     const isActive = this.hasMark(type)
   555     const isActive = this.hasMark(type)
   502     const onMouseDown = e => this.onClickMark(e, type)
   556     const onMouseDown = e => this.onClickMark(e, type)
   503 
   557     const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark");
   504 
   558 
   505     return (
   559     return (
   506       <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}>
   560       // <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}>
       
   561       <span className={markActivation} onMouseDown={onMouseDown} data-active={isActive}>
       
   562 
   507         <span className="material-icons">{icon}</span>
   563         <span className="material-icons">{icon}</span>
   508       </span>
   564       </span>
   509     )
   565     )
   510   }
   566   }
   511 
   567 
   513 
   569 
   514     renderMark = props => {
   570     renderMark = props => {
   515       const { children, mark, attributes } = props
   571       const { children, mark, attributes } = props
   516 
   572 
   517       switch (mark.type) {
   573       switch (mark.type) {
       
   574         default: {
       
   575           break;
       
   576         }
   518         case 'bold':
   577         case 'bold':
   519           return <strong {...attributes}>{children}</strong>
   578           return <strong {...attributes}>{children}</strong>
   520         case 'code':
   579         case 'code':
   521           return <code {...attributes}>{children}</code>
   580           return <code {...attributes}>{children}</code>
   522         case 'italic':
   581         case 'italic':
   540       const { value } = this.state
   599       const { value } = this.state
   541       const parent = value.document.getParent(value.blocks.first().key)
   600       const parent = value.document.getParent(value.blocks.first().key)
   542       isActive = this.hasBlock('list-item') && parent && parent.type === type
   601       isActive = this.hasBlock('list-item') && parent && parent.type === type
   543     }
   602     }
   544     const onMouseDown = e => this.onClickBlock(e, type)
   603     const onMouseDown = e => this.onClickBlock(e, type)
       
   604     const blockActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark");
   545 
   605 
   546     return (
   606     return (
   547       <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}>
   607       <span className={blockActivation} onMouseDown={onMouseDown} data-active={isActive}>
   548         <span className="material-icons">{icon}</span>
   608         <span className="material-icons">{icon}</span>
   549       </span>
   609       </span>
   550     )
   610     )
   551   }
   611   }
   552 
   612 
   553   renderNode = props => {
   613   renderNode = props => {
   554     const { attributes, children, node } = props
   614     const { attributes, children, node } = props
   555 
   615 
   556     switch (node.type) {
   616     switch (node.type) {
       
   617       default: {
       
   618         break;
       
   619       }
   557       case 'block-quote':
   620       case 'block-quote':
   558         return <blockquote {...attributes}>{children}</blockquote>
   621         return <blockquote {...attributes}>{children}</blockquote>
   559       case 'bulleted-list':
   622       case 'bulleted-list':
   560         return <ul {...attributes}>{children}</ul>
   623         return <ul {...attributes}>{children}</ul>
   561       case 'heading-one':
   624       case 'heading-one':
   575    * @return {Element}
   638    * @return {Element}
   576    */
   639    */
   577 
   640 
   578   renderEditor = () => {
   641   renderEditor = () => {
   579     return (
   642     return (
   580       <div className="editor-wrapper sticky-bottom p-2">
   643       <div className="editor-slatejs p-2">
   581       <div className="editor-slatejs">
       
   582         {this.renderHoveringMenu()}
   644         {this.renderHoveringMenu()}
   583         <Editor
   645         <Editor
   584           ref="editor"
   646           ref="editor"
   585           spellCheck
   647           spellCheck
   586           autoFocus
       
   587           placeholder={'Votre espace de prise de note...'}
   648           placeholder={'Votre espace de prise de note...'}
   588           schema={schema}
   649           schema={schema}
   589           plugins={plugins}
   650           plugins={plugins}
   590           value={this.state.value}
   651           value={this.state.value}
   591           onChange={this.onChange}
   652           onChange={this.onChange}
   592           onKeyDown={this.onKeyDown}
   653           onKeyDown={this.onKeyDown}
   593           renderMark={this.renderMark}
   654           renderMark={this.renderMark}
   594           renderNode = {this.renderNode}
   655           renderNode = {this.renderNode}
   595         />
   656         />
   596       </div>
       
   597       </div>
   657       </div>
   598     )
   658     )
   599   }
   659   }
   600 
   660 
   601   renderHoveringMenu = () => {
   661   renderHoveringMenu = () => {