client/src/components/SlateEditor/index.js
changeset 173 0e6703cd0968
parent 172 4b780ebbedc6
child 191 3f71ad81a5a9
equal deleted inserted replaced
172:4b780ebbedc6 173:0e6703cd0968
     1 import { Value } from 'slate';
     1 import { Value } from 'slate';
     2 import Plain from 'slate-plain-serializer';
     2 import Plain from 'slate-plain-serializer';
     3 import { Editor } from 'slate-react';
     3 import { Editor } from 'slate-react';
     4 import React from 'react';
     4 import React from 'react';
     5 import { PortalWithState } from 'react-portal';
     5 import { withNamespaces } from 'react-i18next';
     6 import { Trans, withNamespaces } from 'react-i18next';
     6 import { connect } from 'react-redux';
     7 import * as R from 'ramda';
     7 import * as R from 'ramda';
     8 import HtmlSerializer from './HtmlSerializer';
     8 import HtmlSerializer from './HtmlSerializer';
     9 import AnnotationPlugin from './AnnotationPlugin';
       
    10 import CategoriesTooltip from './CategoriesTooltip';
       
    11 import './SlateEditor.css';
     9 import './SlateEditor.css';
    12 import { now } from '../../utils';
    10 import { now } from '../../utils';
    13 import { defaultAnnotationsCategories } from '../../constants';
    11 import Toolbar from './Toolbar';
    14 
    12 import { getAutoSubmit } from '../../selectors/authSelectors';
    15 const plugins = [];
       
    16 
       
    17 /**
       
    18  * Define the default node type.
       
    19  */
       
    20 
       
    21 const DEFAULT_NODE = 'paragraph'
       
    22 
       
    23 /**
       
    24  * Define a schema.
       
    25  *
       
    26  * @type {Object}
       
    27  */
       
    28 // TODO Check if we can move this to the plugin using the schema option
       
    29 // https://docs.slatejs.org/reference/plugins/plugin.html#schema
       
    30 const schema = {
       
    31 
       
    32   nodes: {
       
    33     'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
       
    34     'list-item': props => <li {...props.attributes}>{props.children}</li>,
       
    35     'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
       
    36   },
       
    37   marks: {
       
    38     bold: {
       
    39       fontWeight: 'bold'
       
    40     },
       
    41     category: props => {
       
    42       const data = props.mark.data;
       
    43       return <span style={{ backgroundColor: data.color }} {...props.attributes}>{props.children}</span>
       
    44     },
       
    45     italic: {
       
    46       fontStyle: 'italic'
       
    47     },
       
    48     underlined: {
       
    49       textDecoration: 'underlined'
       
    50     }
       
    51   }
       
    52 
       
    53 }
       
    54 
       
    55 const initialValue = Value.fromJSON({
       
    56   document: {
       
    57     nodes: [
       
    58       {
       
    59         object: 'block',
       
    60         type: 'paragraph',
       
    61         nodes: [
       
    62           {
       
    63             object: 'text',
       
    64             leaves: [
       
    65               {
       
    66                 text: '',
       
    67               },
       
    68             ],
       
    69           },
       
    70         ],
       
    71       },
       
    72     ],
       
    73   },
       
    74 })
       
    75 
    13 
    76 
    14 
    77 /**
    15 /**
    78  *
    16  *
    79  * @type {Component}
    17  * @type {Component}
    87    * @type {Object}
    25    * @type {Object}
    88    */
    26    */
    89   constructor(props) {
    27   constructor(props) {
    90     super(props);
    28     super(props);
    91 
    29 
    92     const annotationPlugin = AnnotationPlugin({
       
    93       onChange: (text, start, end) => {
       
    94         this.setState({
       
    95           currentSelectionText: text,
       
    96           currentSelectionStart: start,
       
    97           currentSelectionEnd: end,
       
    98         });
       
    99       }
       
   100     });
       
   101 
       
   102     // plugins.push(annotationPlugin);
       
   103 
       
   104 
       
   105     this.state = {
    30     this.state = {
   106       value: props.note ? Value.fromJSON(initialValue) : Plain.deserialize(''),
    31       value: props.note ? Value.fromJSON(JSON.parse(props.note.raw)) : Plain.deserialize(''),
   107       startedAt: null,
    32       startedAt: null,
   108       finishedAt: null,
    33       finishedAt: null,
   109       currentSelectionText: '',
       
   110       currentSelectionStart: 0,
       
   111       currentSelectionEnd: 0,
       
   112       hoveringMenu: null,
       
   113       isPortalOpen: false,
       
   114       categories: [],
       
   115       isCheckboxChecked: false,
       
   116       enterKeyValue: 0,
    34       enterKeyValue: 0,
   117     };
    35     };
   118 
    36 
   119     this.editorRef = React.createRef();
    37     this.editorRef = React.createRef();
   120     this.hoveringMenuRef = React.createRef();
       
   121   }
    38   }
   122 
    39 
   123   get editor() {
    40   get editor() {
   124     if(this.editorRef) {
    41     if(this.editorRef) {
   125       return this.editorRef.current;
    42       return this.editorRef.current;
   126     }
    43     }
   127     return null;
    44     return null;
   128   }
    45   }
   129 
    46 
   130   componentDidMount = () => {
    47   componentDidMount = () => {
   131     this.updateMenu();
       
   132     this.focus();
    48     this.focus();
   133   }
       
   134 
       
   135   componentDidUpdate = () => {
       
   136     this.updateMenu();
       
   137   }
       
   138 
       
   139   getDocumentLength = (document) => {
       
   140     return document.getBlocks().reduce((l, b) => l + b.text.length, 0)
       
   141   }
    49   }
   142 
    50 
   143    /**
    51    /**
   144    * On change, save the new state.
    52    * On change, save the new state.
   145    *
    53    *
   146    * @param {Change} change
    54    * @param {Change} change
   147    */
    55    */
   148 
    56 
   149   onChange = (change) => {
    57   onChange = ({value, operations}) => {
   150 
    58 
   151     const operationTypes = (change && change.operations) ? change.operations.map((o) => o.type).toArray() : [];
    59     const oldState = R.clone(this.state);
   152     console.log("CHANGE", change, operationTypes);
    60 
   153     const { value } = change;
    61     const newState = {
   154 
    62       value
   155     let newState = {
       
   156       value: value,
       
   157       startedAt: this.state.startedAt
       
   158     };
    63     };
   159 
    64 
   160     const isEmpty = this.getDocumentLength(value.document) === 0;
    65     (operations || []).some((op) => {
   161 
    66       if(['insert_text', 'remove_text', 'add_mark', 'remove_mark', 'set_mark', 'insert_node', 'merge_node', 'move_node', 'remove_node', 'set_node', 'split_node'].indexOf(op.type)>=0) {
   162     // Reset timers when the text is empty
    67         const tsnow = now();
   163     if (isEmpty) {
    68         if(this.state.startedAt == null) {
   164       Object.assign(newState, {
    69           newState.startedAt = tsnow;
   165         startedAt: null,
    70         }
   166         finishedAt: null
    71         newState.finishedAt = tsnow;
   167       });
    72         return true;
   168     } else {
    73       }
   169       Object.assign(newState, { finishedAt: now() });
    74       return false;
   170     }
    75     });
   171 
       
   172     // Store start time once when the first character is typed
       
   173     if (!isEmpty && this.state.startedAt === null) {
       
   174       Object.assign(newState, { startedAt: now() });
       
   175     }
       
   176 
       
   177     const oldState = R.clone(this.state);
       
   178 
       
   179     const categories = value.marks.reduce((acc, mark) => {
       
   180       if(mark.type === 'category') {
       
   181         acc.push({
       
   182           key: mark.data.get('key'),
       
   183           name: mark.data.get('name'),
       
   184           color: mark.data.get('color'),
       
   185           text: mark.data.get('text'),
       
   186           selection: {
       
   187             start: mark.data.get('selection').start,
       
   188             end: mark.data.get('selection').end,
       
   189           },
       
   190           comment: mark.data.get('comment')
       
   191         })
       
   192       }
       
   193       return acc;
       
   194     },
       
   195     []);
       
   196 
       
   197     console.log("ON CHANGE categorie", categories);
       
   198 
       
   199     newState['categories'] = categories;
       
   200 
    76 
   201     this.setState(newState, () => {
    77     this.setState(newState, () => {
   202       if (typeof this.props.onChange === 'function') {
    78       if (typeof this.props.onChange === 'function') {
   203         this.props.onChange(R.clone(this.state), oldState, newState);
    79         this.props.onChange(R.clone(this.state), oldState, {value, operations});
   204       }
    80       }
   205     })
    81     })
   206 
    82   }
   207   }
    83 
   208 
       
   209   /**
       
   210    * Check if the current selection has a mark with `type` in it.
       
   211    *
       
   212    * @param {String} type
       
   213    * @return {Boolean}
       
   214    */
       
   215 
       
   216   hasMark = type => {
       
   217     const { value } = this.state;
       
   218     return value.activeMarks.some(mark => mark.type === type);
       
   219   }
       
   220 
       
   221   /**
       
   222    * Check if the any of the currently selected blocks are of `type`.
       
   223    *
       
   224    * @param {String} type
       
   225    * @return {Boolean}
       
   226    */
       
   227 
       
   228   hasBlock = type => {
       
   229     const { value } = this.state
       
   230     return value.blocks.some(node => node.type === type)
       
   231   }
       
   232 
    84 
   233   asPlain = () => {
    85   asPlain = () => {
   234     return Plain.serialize(this.state.value);
    86     return Plain.serialize(this.state.value);
   235   }
    87   }
   236 
    88 
   241   asHtml = () => {
    93   asHtml = () => {
   242     return HtmlSerializer.serialize(this.state.value);
    94     return HtmlSerializer.serialize(this.state.value);
   243   }
    95   }
   244 
    96 
   245   asCategories = () => {
    97   asCategories = () => {
   246     return this.state.categories
    98     return this.state.value.document.getMarksByType('category').map((mark) => mark.data.toJS()).toArray();
   247   }
       
   248 
       
   249   removeCategory = (categories, key, text) => {
       
   250     const categoryIndex = categories.findIndex(category => category.key === key && category.text === text)
       
   251     return categories.delete(categoryIndex)
       
   252   }
    99   }
   253 
   100 
   254   clear = () => {
   101   clear = () => {
   255     const value = Plain.deserialize('');
   102     const value = Plain.deserialize('');
   256     this.onChange({
   103     this.setState({
   257       value,
   104       value,
   258     });
   105       enterKeyValue: 0
       
   106     })
   259   }
   107   }
   260 
   108 
   261   focus = () => {
   109   focus = () => {
   262     if(this.editor) {
   110     if(this.editor) {
   263       this.editor.focus();
   111       this.editor.focus();
   264     }
   112     }
   265   }
   113   }
   266 
   114 
   267   onClickCategoryButton = (openPortal, closePortal, isOpen, e) => {
   115   submitNote = () => {
   268     e.preventDefault();
   116     this.setState({ enterKeyValue: 0 }, () => {
   269     const { categories, value } = this.state
   117       if (typeof this.props.submitNote === 'function') {
   270 
   118         this.props.submitNote();
   271     let newCategories = categories.slice(0);
       
   272 
       
   273     // Can't use toggleMark here, because it expects the same object
       
   274     // @see https://github.com/ianstormtaylor/slate/issues/873
       
   275     if (this.hasMark('category')) {
       
   276       const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category')
       
   277       categoryMarks.forEach(mark => {
       
   278         const key = mark.data.get('key');
       
   279         const text = mark.data.get('text');
       
   280 
       
   281         newCategories = R.reject(category => category.key === key && category.text === text, newCategories);
       
   282         this.editor.removeMark(mark)
       
   283       })
       
   284       this.setState({
       
   285         value: this.editor.value,
       
   286         categories: newCategories
       
   287       });
       
   288       closePortal();
       
   289     } else {
       
   290       openPortal();
       
   291     }
       
   292     // } else {
       
   293     //   isOpen ? closePortal() : openPortal();
       
   294     // }
       
   295   }
       
   296 
       
   297   /**
       
   298    * When a mark button is clicked, toggle the current mark.
       
   299    *
       
   300    * @param {Event} e
       
   301    * @param {String} type
       
   302    */
       
   303 
       
   304   onClickMark = (e, type) => {
       
   305     this.editor.toggleMark(type)
       
   306   }
       
   307 
       
   308   /**
       
   309    * When a block button is clicked, toggle the block type.
       
   310    *
       
   311    * @param {Event} e
       
   312    * @param {String} type
       
   313    */
       
   314 
       
   315   onClickBlock = (e, type) => {
       
   316     e.preventDefault()
       
   317 
       
   318     const { editor } = this;
       
   319     const { value } = editor;
       
   320     const { document } = value;
       
   321 
       
   322     // Handle everything but list buttons.
       
   323     if (type !== 'bulleted-list' && type !== 'numbered-list') {
       
   324       const isActive = this.hasBlock(type)
       
   325       const isList = this.hasBlock('list-item')
       
   326 
       
   327       if (isList) {
       
   328         editor
       
   329           .setBlocks(isActive ? DEFAULT_NODE : type)
       
   330           .unwrapBlock('bulleted-list')
       
   331           .unwrapBlock('numbered-list')
       
   332       }
       
   333 
       
   334       else {
       
   335        editor
       
   336           .setBlocks(isActive ? DEFAULT_NODE : type)
       
   337       }
       
   338     }
       
   339 
       
   340     // Handle the extra wrapping required for list buttons.
       
   341     else {
       
   342       const isList = this.hasBlock('list-item')
       
   343       const isType = value.blocks.some((block) => {
       
   344         return !!document.getClosest(block.key, parent => parent.type === type)
       
   345       })
       
   346 
       
   347       if (isList && isType) {
       
   348        editor
       
   349           .setBlocks(DEFAULT_NODE)
       
   350           .unwrapBlock('bulleted-list')
       
   351           .unwrapBlock('numbered-list')
       
   352 
       
   353       } else if (isList) {
       
   354         editor
       
   355           .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list')
       
   356           .wrapBlock(type)
       
   357 
       
   358       } else {
       
   359         editor
       
   360           .setBlocks('list-item')
       
   361           .wrapBlock(type)
       
   362 
       
   363       }
       
   364     }
       
   365     // this.onChange(change)
       
   366   }
       
   367 
       
   368   onPortalOpen = () => {
       
   369     console.log("onPORTAL OPEN", this);
       
   370     this.updateMenu();
       
   371     // When the portal opens, cache the menu element.
       
   372     // this.setState({ hoveringMenu: this.portal.firstChild })
       
   373   }
       
   374 
       
   375   onPortalClose = (portal) => {
       
   376     console.log("onPORTAL CLOSE", this);
       
   377     // let { value } = this.state
       
   378 
       
   379     // this.setState({
       
   380     //   value: value.change,
       
   381     //   isPortalOpen: false
       
   382     // })
       
   383   }
       
   384 
       
   385   getSelectionParams = () => {
       
   386 
       
   387     const { value } = this.editor
       
   388     const { selection } = value
       
   389     const { start, end} = selection
       
   390 
       
   391     if (selection.isCollapsed) {
       
   392       return {};
       
   393     }
       
   394 
       
   395     const nodes = [];
       
   396     let hasStarted = false;
       
   397     let hasEnded = false;
       
   398 
       
   399     // Keep only the relevant nodes,
       
   400     // i.e. nodes which are contained within selection
       
   401     value.document.nodes.forEach((node) => {
       
   402       if (start.isInNode(node)) {
       
   403         hasStarted = true;
       
   404       }
       
   405       if (hasStarted && !hasEnded) {
       
   406         nodes.push(node);
       
   407       }
       
   408       if (end.isAtEndOfNode(node)) {
       
   409         hasEnded = true;
       
   410       }
   119       }
   411     });
   120     });
   412 
       
   413     // Concatenate the nodes text
       
   414     const text = nodes.map((node) => {
       
   415       let textStart = start.isInNode(node) ? start.offset : 0;
       
   416       let textEnd = end.isInNode(node) ? end.offset : node.text.length;
       
   417       return node.text.substring(textStart,textEnd);
       
   418     }).join('\n');
       
   419 
       
   420     return {
       
   421       currentSelectionText: text,
       
   422       currentSelectionStart: start.offset,
       
   423       currentSelectionEnd: end.offset
       
   424     };
       
   425   }
       
   426 
       
   427   onCategoryClick = (closePortal, category) => {
       
   428 
       
   429     console.log("ON CATEGORY CLICK");
       
   430     const { value } = this.state;
       
   431     let { categories } = this.state;
       
   432 
       
   433     const { currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.getSelectionParams();
       
   434 
       
   435     if(!currentSelectionText) {
       
   436       closePortal();
       
   437       return;
       
   438     }
       
   439     console.log("ACTIVE MARKS", category, currentSelectionText, currentSelectionStart, currentSelectionEnd)
       
   440 
       
   441     const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category')
       
   442     categoryMarks.forEach(mark => this.editor.removeMark(mark));
       
   443 
       
   444     this.editor.addMark({
       
   445       type: 'category',
       
   446       data: {
       
   447         text: currentSelectionText,
       
   448         selection: {
       
   449           start: currentSelectionStart,
       
   450           end: currentSelectionEnd,
       
   451         },
       
   452         color: category.color,
       
   453         key: category.key,
       
   454         name: category.name,
       
   455         comment: category.comment
       
   456       }
       
   457     })
       
   458 
       
   459     Object.assign(category, {
       
   460       text: currentSelectionText,
       
   461       selection: {
       
   462         start: currentSelectionStart,
       
   463         end: currentSelectionEnd,
       
   464       },
       
   465     });
       
   466     categories.push(category);
       
   467 
       
   468     console.log("CATEGORIES", categories)
       
   469 
       
   470     this.setState({
       
   471       categories: categories,
       
   472       value: this.editor.value
       
   473     }, closePortal);
       
   474   }
       
   475 
       
   476   onButtonClick = () => {
       
   477     if (typeof this.props.onButtonClick === 'function') {
       
   478       this.props.onButtonClick();
       
   479     }
       
   480   }
       
   481 
       
   482   onCheckboxChange = (e) => {
       
   483     if (typeof this.props.onCheckboxChange === 'function') {
       
   484       this.props.onCheckboxChange(e);
       
   485     }
       
   486   }
   121   }
   487 
   122 
   488   /**
   123   /**
   489    * On key down, if it's a formatting command toggle a mark.
   124    * On key down, if it's a formatting command toggle a mark.
   490    *
   125    *
   491    * @param {Event} e
   126    * @param {Event} e
   492    * @param {Change} change
   127    * @param {Change} change
   493    * @return {Change}
   128    * @return {Change}
   494    */
   129    */
   495 
   130 
   496   onKeyDown = (e, change) => {
   131   onKeyUp = (e, editor, next) => {
   497 
   132 
   498     const {value} = this.state;
   133     const { value } = this.state;
   499 
   134     const noteText = value.document.text.trim();
   500     if (e.key === 'Enter' && value.document.text !== '') {
   135 
   501       this.setState({enterKeyValue: 1})
   136     if(e.key === "Enter" && noteText.length !== 0) {
   502     }
   137       this.setState({ enterKeyValue: this.state.enterKeyValue + 1 });
   503 
   138     } else if ( e.getModifierState() || (e.key !== "Control" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Alt") ) {
   504     if (e.key !== 'Enter') {
   139       this.setState({ enterKeyValue: 0 });
   505       this.setState({
   140     }
   506         enterKeyValue: 0,
   141     return next();
   507       })
   142   }
   508 
   143 
   509     }
   144   /**
   510 
   145    * On key down, if it's a formatting command toggle a mark.
   511     //TODO review the double enter case.
   146    *
   512     if (e.key === 'Enter' && !this.props.isChecked && this.state.enterKeyValue === 1 && typeof this.props.onEnterKeyDown === 'function') {
   147    * @param {Event} e
       
   148    * @param {Change} change
       
   149    * @return {Change}
       
   150    */
       
   151 
       
   152   onKeyDown = (e, editor, next) => {
       
   153 
       
   154     const { value, enterKeyValue } = this.state;
       
   155     const { autoSubmit } = this.props;
       
   156     const noteText = value.document.text.trim();
       
   157 
       
   158     // we prevent empty first lines
       
   159     if(e.key === "Enter" && noteText.length === 0) {
   513       e.preventDefault();
   160       e.preventDefault();
   514       this.props.onEnterKeyDown();
   161       return next();
   515       this.setState({
   162     }
   516         enterKeyValue: 0,
   163 
   517       })
   164     // Enter submit the note
   518 
   165     if(e.key === "Enter" && ( enterKeyValue === 2 || e.ctrlKey || autoSubmit ) && noteText.length !== 0) {
   519 
       
   520       return change
       
   521     }
       
   522 
       
   523     else if (e.key === 'Enter' && value.document.text !== '' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') {
       
   524 
       
   525       e.preventDefault();
   166       e.preventDefault();
   526       this.props.onEnterKeyDown();
   167       this.submitNote();
   527 
   168       return next();
   528       return change
   169     }
   529     }
   170 
   530 
   171     if (!e.ctrlKey) {
   531     if (!e.ctrlKey) return
   172       return next();
   532         // Decide what to do based on the key code...
   173     }
   533         switch (e.key) {
   174 
   534           default: {
   175     e.preventDefault();
   535             break;
   176 
   536           }
   177     // Decide what to do based on the key code...
   537           // When "B" is pressed, add a "bold" mark to the text.
   178     switch (e.key) {
   538           case 'b': {
   179       default: {
   539             e.preventDefault()
   180         break;
   540             change.toggleMark('bold')
   181       }
   541 
   182       // When "B" is pressed, add a "bold" mark to the text.
   542             return true
   183       case 'b': {
   543           }
   184         editor.toggleMark('bold');
   544           case 'i': {
   185         break;
   545             // When "U" is pressed, add an "italic" mark to the text.
   186       }
   546             e.preventDefault()
   187       case 'i': {
   547             change.toggleMark('italic')
   188         // When "U" is pressed, add an "italic" mark to the text.
   548 
   189         editor.toggleMark('italic');
   549             return true
   190         break;
   550           }
   191       }
   551           case 'u': {
   192       case 'u': {
   552             // When "U" is pressed, add an "underline" mark to the text.
   193         // When "U" is pressed, add an "underline" mark to the text.
   553             e.preventDefault()
   194         editor.toggleMark('underlined');
   554             change.toggleMark('underlined')
   195         break;
   555 
   196       }
   556             return true
   197     }
   557           }
   198 
   558           case 'Enter': {
   199     return next();
   559             // When "ENTER" is pressed, autosubmit the note.
   200 
   560             if (value.document.text !== '' && typeof this.props.onEnterKeyDown === 'function') {
       
   561               e.preventDefault()
       
   562               this.props.onEnterKeyDown();
       
   563               this.setState({
       
   564                 enterKeyValue: 0,
       
   565               })
       
   566 
       
   567               return true
       
   568             }
       
   569         }
       
   570       }
       
   571   }
       
   572 
       
   573   /**
       
   574    * Render.
       
   575    *
       
   576    * @return {Element}
       
   577    */
       
   578 
       
   579   render = () => {
       
   580     return (
       
   581       <div className="bg-secondary mb-5">
       
   582         <div className="sticky-top">
       
   583         {this.renderToolbar()}
       
   584         </div>
       
   585         {this.renderEditor()}
       
   586     </div>
       
   587     )
       
   588   }
       
   589 
       
   590   /**
       
   591    * Render the toolbar.
       
   592    *
       
   593    * @return {Element}
       
   594    */
       
   595 
       
   596   renderToolbar = () => {
       
   597     return (
       
   598       <div className="menu toolbar-menu d-flex sticky-top bg-secondary">
       
   599           {this.renderMarkButton('bold', 'format_bold')}
       
   600           {this.renderMarkButton('italic', 'format_italic')}
       
   601           {this.renderMarkButton('underlined', 'format_underlined')}
       
   602           {this.renderCategoryButton()}
       
   603 
       
   604           {this.renderBlockButton('numbered-list', 'format_list_numbered')}
       
   605           {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
       
   606 
       
   607           {this.renderToolbarButtons()}
       
   608       </div>
       
   609     )
       
   610   }
       
   611 
       
   612   renderToolbarCheckbox = () => {
       
   613     return (
       
   614       <div className="checkbox float-right">
       
   615         <label className="mr-2">
       
   616           <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /><small className="text-muted ml-1"><Trans i18nKey="slate_editor.press_enter_msg">Appuyer sur <kbd className="bg-irinotes-form text-muted ml-1">Entrée</kbd> pour ajouter une note</Trans></small>
       
   617         </label>
       
   618       </div>
       
   619     )
       
   620   }
       
   621 
       
   622   renderToolbarButtons = () => {
       
   623     const t = this.props.t;
       
   624     return (
       
   625       <div>
       
   626         <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold float-right text-capitalize" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
       
   627           { this.props.note ? t('common.save') : t('common.add') }
       
   628         </button>
       
   629         { !this.props.note && this.renderToolbarCheckbox() }
       
   630       </div>
       
   631     );
       
   632   }
       
   633 
       
   634   /**
       
   635    * Render a mark-toggling toolbar button.
       
   636    *
       
   637    * @param {String} type
       
   638    * @param {String} icon
       
   639    * @return {Element}
       
   640    */
       
   641 
       
   642   renderMarkButton = (type, icon) => {
       
   643     const isActive = this.hasMark(type)
       
   644     const onMouseDown = e => this.onClickMark(e, type)
       
   645     const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark");
       
   646 
       
   647     return (
       
   648       // <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}>
       
   649       <span className={markActivation} onMouseDown={onMouseDown} data-active={isActive}>
       
   650 
       
   651         <span className="material-icons">{icon}</span>
       
   652       </span>
       
   653     )
       
   654   }
       
   655 
       
   656     /**
       
   657    * Render a mark-toggling toolbar button.
       
   658    *
       
   659    * @param {String} type
       
   660    * @param {String} icon
       
   661    * @return {Element}
       
   662    */
       
   663 
       
   664   renderCategoryButton = () => {
       
   665     const isActive = this.hasMark('category');
       
   666     //const onMouseDown = e => this.onClickMark(e, type)
       
   667     const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark");
       
   668 
       
   669     return (
       
   670       <PortalWithState
       
   671         // closeOnOutsideClick
       
   672         closeOnEsc
       
   673         onOpen={this.onPortalOpen}
       
   674         onClose={this.onPortalClose}
       
   675       >
       
   676         {({ openPortal, closePortal, isOpen, portal }) => {
       
   677           console.log("PORTAL", isOpen);
       
   678           const onMouseDown = R.partial(this.onClickCategoryButton, [openPortal, closePortal, isOpen]);
       
   679           const onCategoryClick = R.partial(this.onCategoryClick, [closePortal,]);
       
   680           return (
       
   681             <React.Fragment>
       
   682               <span className={markActivation} onMouseDown={onMouseDown} data-active={isActive}>
       
   683                 <span className="material-icons">label</span>
       
   684               </span>
       
   685               {portal(
       
   686                 <div className="hovering-menu" ref={this.hoveringMenuRef}>
       
   687                   <CategoriesTooltip categories={this.props.annotationCategories || defaultAnnotationsCategories} onCategoryClick={onCategoryClick} />
       
   688                 </div>
       
   689               )}
       
   690             </React.Fragment>
       
   691           )}
       
   692         }
       
   693       </PortalWithState>
       
   694     )
       
   695   }
   201   }
   696 
   202 
   697   // Add a `renderMark` method to render marks.
   203   // Add a `renderMark` method to render marks.
   698 
   204 
   699   renderMark = (props, editor, next) => {
   205   renderMark = (props, editor, next) => {
   700     const { children, mark, attributes } = props
   206     const { children, mark, attributes } = props
   701 
   207 
   702     console.log("renderMark", mark, mark.type, mark.data.color);
       
   703     switch (mark.type) {
   208     switch (mark.type) {
   704       case 'bold':
   209       case 'bold':
   705         return <strong {...attributes}>{children}</strong>
   210         return <strong {...attributes}>{children}</strong>
   706       case 'code':
   211       case 'code':
   707         return <code {...attributes}>{children}</code>
   212         return <code {...attributes}>{children}</code>
   716         return <span {...attributes} style={ spanStyle } >{children}</span>
   221         return <span {...attributes} style={ spanStyle } >{children}</span>
   717       default:
   222       default:
   718         return next();
   223         return next();
   719     }
   224     }
   720   }
   225   }
   721   /**
       
   722    * Render a block-toggling toolbar button.
       
   723    *
       
   724    * @param {String} type
       
   725    * @param {String} icon
       
   726    * @return {Element}
       
   727    */
       
   728 
       
   729   renderBlockButton = (type, icon) => {
       
   730     let isActive = this.hasBlock(type)
       
   731 
       
   732     if (['numbered-list', 'bulleted-list'].includes(type)) {
       
   733       const { value } = this.state;
       
   734       const firstBlock = value.blocks.first();
       
   735       if(firstBlock) {
       
   736         const parent = value.document.getParent(firstBlock.key);
       
   737         isActive = this.hasBlock('list-item') && parent && parent.type === type;
       
   738       }
       
   739     }
       
   740     const onMouseDown = e => this.onClickBlock(e, type)
       
   741     const blockActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark");
       
   742 
       
   743     return (
       
   744       <span className={blockActivation} onMouseDown={onMouseDown} data-active={isActive}>
       
   745         <span className="material-icons">{icon}</span>
       
   746       </span>
       
   747     )
       
   748   }
       
   749 
   226 
   750   renderNode = (props, editor, next) => {
   227   renderNode = (props, editor, next) => {
   751     const { attributes, children, node } = props
   228     const { attributes, children, node } = props
   752 
   229 
   753     switch (node.type) {
   230     switch (node.type) {
   764       case 'numbered-list':
   241       case 'numbered-list':
   765         return <ol {...attributes}>{children}</ol>
   242         return <ol {...attributes}>{children}</ol>
   766       default:
   243       default:
   767         return next();
   244         return next();
   768     }
   245     }
   769 }
   246   }
   770 
   247 
   771   /**
   248     /**
   772    * Render the Slate editor.
   249    * Render.
   773    *
   250    *
   774    * @return {Element}
   251    * @return {Element}
   775    */
   252    */
   776 
   253 
   777   renderEditor = () => {
   254   render = () => (
   778     const t = this.props.t;
   255     <div className="bg-secondary mb-5">
   779     return (
   256       <div className="sticky-top">
       
   257         <Toolbar
       
   258           value={this.state.value}
       
   259           editor={this.editor}
       
   260           note={this.props.note}
       
   261           annotationCategories={this.props.annotationCategories}
       
   262           isButtonDisabled={this.props.isButtonDisabled}
       
   263           submitNote={this.submitNote}
       
   264         />
       
   265       </div>
   780       <div className="editor-slatejs p-2">
   266       <div className="editor-slatejs p-2">
   781         {/* {this.renderHoveringMenu()} */}
       
   782         <Editor
   267         <Editor
   783           ref={this.editorRef}
   268           ref={this.editorRef}
   784           spellCheck
   269           spellCheck
   785           placeholder={t('slate_editor.placeholder')}
   270           placeholder={this.props.t('slate_editor.placeholder')}
   786           // schema={schema}
       
   787           plugins={plugins}
       
   788           value={this.state.value}
   271           value={this.state.value}
   789           onChange={this.onChange}
   272           onChange={this.onChange}
   790           // onKeyDown={this.onKeyDown}
   273           onKeyDown={this.onKeyDown}
       
   274           onKeyUp={this.onKeyUp}
   791           renderMark={this.renderMark}
   275           renderMark={this.renderMark}
   792           renderNode = {this.renderNode}
   276           renderNode = {this.renderNode}
   793         />
   277         />
   794       </div>
   278       </div>
   795     )
   279     </div>
   796   }
   280   );
   797 
   281 
   798   // renderHoveringMenu = () => {
       
   799   //   return (
       
   800   //     <Portal ref="portal"
       
   801   //       isOpened={this.state.isPortalOpen} isOpen={this.state.isPortalOpen}
       
   802   //       onOpen={this.onPortalOpen}
       
   803   //       onClose={this.onPortalClose}
       
   804   //       closeOnOutsideClick={false} closeOnEsc={true}>
       
   805   //       <div className="hovering-menu">
       
   806   //         <CategoriesTooltip categories={this.props.annotationCategories || defaultAnnotationsCategories} onCategoryClick={this.onCategoryClick} />
       
   807   //       </div>
       
   808   //     </Portal>
       
   809   //   )
       
   810   // }
       
   811 
       
   812   updateMenu = () => {
       
   813 
       
   814     // const { hoveringMenu } = this.state
       
   815     const hoveringMenu = this.hoveringMenuRef.current;
       
   816 
       
   817     if (!hoveringMenu) return
       
   818 
       
   819     // if (state.isBlurred || state.isCollapsed) {
       
   820     //   hoveringMenu.removeAttribute('style')
       
   821     //   return
       
   822     // }
       
   823 
       
   824     const selection = window.getSelection()
       
   825 
       
   826     if (selection.isCollapsed) {
       
   827       return
       
   828     }
       
   829 
       
   830     const range = selection.getRangeAt(0)
       
   831     const rect = range.getBoundingClientRect()
       
   832 
       
   833     hoveringMenu.style.opacity = 1
       
   834     hoveringMenu.style.top = `${rect.top + rect.height + window.scrollY + hoveringMenu.offsetHeight}px`
       
   835     hoveringMenu.style.left = `${rect.left + window.scrollX - hoveringMenu.offsetWidth / 2 + rect.width / 2}px`
       
   836   }
       
   837 
   282 
   838 }
   283 }
   839 
   284 
   840 /**
   285 /**
   841  * Export.
   286  * Export.
   842  */
   287  */
       
   288 function mapStateToProps(state, props) {
       
   289 
       
   290   const autoSubmit = getAutoSubmit(state);
       
   291 
       
   292   return {
       
   293     autoSubmit,
       
   294   };
       
   295 }
   843 
   296 
   844 export default withNamespaces("", {
   297 export default withNamespaces("", {
   845   innerRef: (ref) => {
   298   innerRef: (ref) => {
   846     const editorRef = (ref && ref.props) ? ref.props.editorRef : null;
   299     if(!ref) {
       
   300       return;
       
   301     }
       
   302     const wrappedRef = ref.getWrappedInstance();
       
   303     const editorRef = (wrappedRef && wrappedRef.props) ? wrappedRef.props.editorRef : null;
   847     if(editorRef && editorRef.hasOwnProperty('current')) {
   304     if(editorRef && editorRef.hasOwnProperty('current')) {
   848       editorRef.current = ref;
   305       editorRef.current = wrappedRef;
   849     }
   306     }
   850   }
   307   }
   851 })(SlateEditor);
   308 })(connect(mapStateToProps, null, null, { withRef: true })(SlateEditor));
   852 // export default SlateEditor;