client/src/components/SlateEditor.js
changeset 157 5c3af4f10e92
parent 143 cfcbf4bc66f1
child 159 a4705c2b4544
--- a/client/src/components/SlateEditor.js	Wed Sep 05 13:48:10 2018 +0200
+++ b/client/src/components/SlateEditor.js	Tue Sep 25 02:02:13 2018 +0200
@@ -1,4 +1,6 @@
-import { Editor, Plain, Raw } from 'slate'
+import { Value } from 'slate'
+import Plain from 'slate-plain-serializer'
+import { Editor } from 'slate-react'
 import React from 'react'
 import Portal from 'react-portal'
 import Immutable from 'immutable'
@@ -41,11 +43,32 @@
       fontStyle: 'italic'
     },
     underlined: {
-      textDecoration: 'underline'
+      textDecoration: 'underlined'
     }
   }
 }
 
+const initialValue = Value.fromJSON({
+  document: {
+    nodes: [
+      {
+        object: 'block',
+        type: 'paragraph',
+        nodes: [
+          {
+            object: 'text',
+            leaves: [
+              {
+                text: '',
+              },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+})
+
 /**
  * The rich text example.
  *
@@ -74,8 +97,9 @@
 
     plugins.push(annotationPlugin);
 
+
     this.state = {
-      state: props.note ? Raw.deserialize(props.note.raw) : Plain.deserialize(''),
+      value: props.note ? initialValue : Plain.deserialize(''),
       startedAt: null,
       finishedAt: null,
       currentSelectionText: '',
@@ -97,44 +121,20 @@
     this.updateMenu();
   }
 
-  /**
-   * Check if the current selection has a mark with `type` in it.
+   /**
+   * On change, save the new state.
    *
-   * @param {String} type
-   * @return {Boolean}
+   * @param {Change} change
    */
 
-  hasMark = (type) => {
-    const { state } = this.state
-    return state.marks.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 { state } = this.state
-    return state.blocks.some(node => node.type === type)
-  }
-
-  /**
-   * On change, save the new state.
-   *
-   * @param {State} state
-   */
-
-  onChange = (state) => {
+  onChange = ({value}) => {
 
     let newState = {
-      state: state,
+      value: value,
       startedAt: this.state.startedAt
     };
 
-    const isEmpty = state.document.length === 0;
+    const isEmpty = value.document.length === 0;
 
     // Reset timers when the text is empty
     if (isEmpty) {
@@ -158,16 +158,40 @@
     }
   }
 
+  /**
+   * Check if the current selection has a mark with `type` in it.
+   *
+   * @param {String} type
+   * @return {Boolean}
+   */
+
+  hasMark = type => {
+    const { value } = this.state
+    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.state
+    return value.blocks.some(node => node.type === type)
+}
+
   asPlain = () => {
-    return Plain.serialize(this.state.state);
+    return Plain.serialize(this.state.value);
   }
 
   asRaw = () => {
-    return Raw.serialize(this.state.state);
+    return JSON.stringify(this.state.value.toJSON());
   }
 
   asHtml = () => {
-    return HtmlSerializer.serialize(this.state.state);
+    return HtmlSerializer.serialize(this.state.value);
   }
 
   asCategories = () => {
@@ -180,8 +204,8 @@
   }
 
   clear = () => {
-    const state = Plain.deserialize('');
-    this.onChange(state);
+    const value = Plain.deserialize('');
+    this.onChange({value});
   }
 
   focus = () => {
@@ -193,46 +217,48 @@
    *
    * @param {Event} e
    * @param {Object} data
-   * @param {State} state
-   * @return {State}
+   * @param {Change} change
+   * @return {Change}
    */
 
-  onKeyDown = (e, data, state) => {
+  onKeyDown = (e, change) => {
+    //   if (data.key === 'enter' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') {
+
+    //   e.preventDefault();
+    //   this.props.onEnterKeyDown();
+
+    //   return change;
+    // }
+
+    // if (!data.isMod) return
+    if (!e.ctrlKey) return
+        // Decide what to do based on the key code...
+        switch (e.key) {
+          // When "B" is pressed, add a "bold" mark to the text.
+          case 'b': {
+            e.preventDefault()
+            change.toggleMark('bold')
 
-    if (data.key === 'enter' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') {
-      e.preventDefault();
-      this.props.onEnterKeyDown();
+            return true
+          }
+          case 'i': {
+            // When "U" is pressed, add an "italic" mark to the text.
+            e.preventDefault()
+            change.toggleMark('italic')
 
-      return state;
+            return true
+          }
+          case 'u': {
+            // When "U" is pressed, add an "underline" mark to the text.
+            e.preventDefault()
+            change.toggleMark('underlined')
+
+            return true
+          }
+        }
     }
 
-    if (!data.isMod) return
-    let mark
-
-    switch (data.key) {
-      case 'b':
-        mark = 'bold'
-        break
-      case 'i':
-        mark = 'italic'
-        break
-      case 'u':
-        mark = 'underlined'
-        break
-      default:
-        return
-    }
-
-    state = state
-      .transform()
-      .toggleMark(mark)
-      .apply()
-
-    e.preventDefault()
-    return state
-  }
-
-  /**
+      /**
    * When a mark button is clicked, toggle the current mark.
    *
    * @param {Event} e
@@ -240,10 +266,10 @@
    */
 
   onClickMark = (e, type) => {
-    e.preventDefault()
-    const { state } = this.state
+
+      e.preventDefault()
+    const { value } = this.state
     let { categories } = this.state
-    const transform = state.transform()
 
     let isPortalOpen = false;
 
@@ -251,24 +277,26 @@
       // Can't use toggleMark here, because it expects the same object
       // @see https://github.com/ianstormtaylor/slate/issues/873
       if (this.hasMark('category')) {
-        const categoryMarks = state.marks.filter(mark => mark.type === 'category')
+        const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category')
         categoryMarks.forEach(mark => {
           const key = mark.data.get('key');
           const text = mark.data.get('text');
 
           categories = this.removeCategory(categories, key, text)
-          transform.removeMark(mark)
+          const change = value.change().removeMark(mark)
+          this.onChange(change)
         })
 
       } else {
         isPortalOpen = !this.state.isPortalOpen;
       }
     } else {
-      transform.toggleMark(type)
+      const change = value.change().toggleMark(type)
+      this.onChange(change)
     }
 
     this.setState({
-      state: transform.apply(),
+      state: value.change,
       isPortalOpen: isPortalOpen,
       categories: categories
     })
@@ -283,9 +311,9 @@
 
   onClickBlock = (e, type) => {
     e.preventDefault()
-    let { state } = this.state
-    const transform = state.transform()
-    const { document } = state
+    const { value } = this.state
+    const change = value.change()
+    const { document } = value
 
     // Handle everything but list buttons.
     if (type !== 'bulleted-list' && type !== 'numbered-list') {
@@ -293,43 +321,46 @@
       const isList = this.hasBlock('list-item')
 
       if (isList) {
-        transform
-          .setBlock(isActive ? DEFAULT_NODE : type)
+        change
+          .setBlocks(isActive ? DEFAULT_NODE : type)
           .unwrapBlock('bulleted-list')
           .unwrapBlock('numbered-list')
       }
 
       else {
-        transform
-          .setBlock(isActive ? DEFAULT_NODE : type)
+       change
+          .setBlocks(isActive ? DEFAULT_NODE : type)
       }
     }
 
     // Handle the extra wrapping required for list buttons.
     else {
       const isList = this.hasBlock('list-item')
-      const isType = state.blocks.some((block) => {
+      const isType = value.blocks.some((block) => {
         return !!document.getClosest(block.key, parent => parent.type === type)
       })
 
       if (isList && isType) {
-        transform
-          .setBlock(DEFAULT_NODE)
+       change
+          .setBlocks(DEFAULT_NODE)
           .unwrapBlock('bulleted-list')
           .unwrapBlock('numbered-list')
+
       } else if (isList) {
-        transform
+        change
           .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list')
           .wrapBlock(type)
+
       } else {
-        transform
-          .setBlock('list-item')
+        change
+          .setBlocks('list-item')
           .wrapBlock(type)
+
       }
     }
 
-    state = transform.apply()
-    this.setState({ state })
+
+    this.onChange(change)
   }
 
   onPortalOpen = (portal) => {
@@ -338,25 +369,24 @@
   }
 
   onPortalClose = (portal) => {
-    let { state } = this.state
-    const transform = state.transform();
+    let { value } = this.state
 
     this.setState({
-      state: transform.apply(),
+      value: value.change,
       isPortalOpen: false
     })
   }
 
   onCategoryClick = (category) => {
 
-    const { state, currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.state;
+    const { value, currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.state;
+    const change = value.change()
     let { categories } = this.state;
-    const transform = state.transform();
 
-    const categoryMarks = state.marks.filter(mark => mark.type === 'category')
-    categoryMarks.forEach(mark => transform.removeMark(mark));
+    const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category')
+    categoryMarks.forEach(mark => change.removeMark(mark));
 
-    transform.addMark({
+    change.addMark({
       type: 'category',
       data: {
         text: currentSelectionText,
@@ -368,6 +398,7 @@
         key: category.key
       }
     })
+    this.onChange(change)
 
     Object.assign(category, {
       text: currentSelectionText,
@@ -379,7 +410,7 @@
     categories = categories.push(category);
 
     this.setState({
-      state: transform.apply(),
+      value: value,
       isPortalOpen: false,
       categories: categories
     });
@@ -405,10 +436,12 @@
 
   render = () => {
     return (
-      <div>
+      <div className="bg-secondary mb-5">
+        <div className="sticky-top">
         {this.renderToolbar()}
+        </div>
         {this.renderEditor()}
-      </div>
+    </div>
     )
   }
 
@@ -420,25 +453,26 @@
 
   renderToolbar = () => {
     return (
-      <div className="menu toolbar-menu">
-        {this.renderMarkButton('bold', 'format_bold')}
-        {this.renderMarkButton('italic', 'format_italic')}
-        {this.renderMarkButton('underlined', 'format_underlined')}
-        {this.renderMarkButton('category', 'label')}
+      <div className="menu toolbar-menu d-flex bg-secondary">
+          {this.renderMarkButton('bold', 'format_bold')}
+          {this.renderMarkButton('italic', 'format_italic')}
+          {this.renderMarkButton('underlined', 'format_underlined')}
+          {this.renderMarkButton('category', 'label')}
 
-        {this.renderBlockButton('numbered-list', 'format_list_numbered')}
-        {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
 
-        {this.renderToolbarButtons()}
+          {this.renderBlockButton('numbered-list', 'format_list_numbered')}
+          {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
+
+          {this.renderToolbarButtons()}
       </div>
     )
   }
 
   renderToolbarCheckbox = () => {
     return (
-      <div className="checkbox">
-        <label>
-          <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /> <kbd>Entrée</kbd>= Ajouter une note
+      <div className="checkbox float-right">
+        <label className="mr-2">
+          <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /><small className="text-muted ml-1"> Appuyer sur <kbd className="bg-danger text-muted ml-1">Entrée</kbd> pour ajouter une note</small>
         </label>
       </div>
     )
@@ -447,10 +481,10 @@
   renderToolbarButtons = () => {
     return (
       <div>
-        { !this.props.note && this.renderToolbarCheckbox() }
-        <button type="button" className="btn btn-primary btn-lg" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
+        <button type="button" className="btn btn-primary btn-sm text-secondary float-right mr-5" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
           { this.props.note ? 'Save note' : 'Ajouter' }
         </button>
+        { !this.props.note && this.renderToolbarCheckbox() }
       </div>
     );
   }
@@ -467,13 +501,30 @@
     const isActive = this.hasMark(type)
     const onMouseDown = e => this.onClickMark(e, type)
 
+
     return (
-      <span className="button" onMouseDown={onMouseDown} data-active={isActive}>
+      <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}>
         <span className="material-icons">{icon}</span>
       </span>
     )
   }
 
+    // Add a `renderMark` method to render marks.
+
+    renderMark = props => {
+      const { children, mark, attributes } = props
+
+      switch (mark.type) {
+        case 'bold':
+          return <strong {...attributes}>{children}</strong>
+        case 'code':
+          return <code {...attributes}>{children}</code>
+        case 'italic':
+          return <em {...attributes}>{children}</em>
+        case 'underlined':
+          return <ins {...attributes}>{children}</ins>
+      }
+  }
   /**
    * Render a block-toggling toolbar button.
    *
@@ -483,16 +534,41 @@
    */
 
   renderBlockButton = (type, icon) => {
-    const isActive = this.hasBlock(type)
+    let isActive = this.hasBlock(type)
+
+    if (['numbered-list', 'bulleted-list'].includes(type)) {
+      const { value } = this.state
+      const parent = value.document.getParent(value.blocks.first().key)
+      isActive = this.hasBlock('list-item') && parent && parent.type === type
+    }
     const onMouseDown = e => this.onClickBlock(e, type)
 
     return (
-      <span className="button" onMouseDown={onMouseDown} data-active={isActive}>
+      <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}>
         <span className="material-icons">{icon}</span>
       </span>
     )
   }
 
+  renderNode = props => {
+    const { attributes, children, node } = props
+
+    switch (node.type) {
+      case 'block-quote':
+        return <blockquote {...attributes}>{children}</blockquote>
+      case 'bulleted-list':
+        return <ul {...attributes}>{children}</ul>
+      case 'heading-one':
+        return <h1 {...attributes}>{children}</h1>
+      case 'heading-two':
+        return <h2 {...attributes}>{children}</h2>
+      case 'list-item':
+        return <li {...attributes}>{children}</li>
+      case 'numbered-list':
+        return <ol {...attributes}>{children}</ol>
+    }
+}
+
   /**
    * Render the Slate editor.
    *
@@ -501,19 +577,24 @@
 
   renderEditor = () => {
     return (
+      <div className="editor-wrapper sticky-bottom p-2">
       <div className="editor-slatejs">
         {this.renderHoveringMenu()}
         <Editor
           ref="editor"
           spellCheck
-          placeholder={'Enter some rich text...'}
+          autoFocus
+          placeholder={'Votre espace de prise de note...'}
           schema={schema}
           plugins={plugins}
-          state={this.state.state}
+          value={this.state.value}
           onChange={this.onChange}
           onKeyDown={this.onKeyDown}
+          renderMark={this.renderMark}
+          renderNode = {this.renderNode}
         />
       </div>
+      </div>
     )
   }