Update slate editor
authorsalimr <riwad.salim@yahoo.fr>
Tue, 25 Sep 2018 02:02:13 +0200
changeset 157 5c3af4f10e92
parent 156 384f4539b76a
child 158 964438ef8401
Update slate editor
client/package.json
client/src/AnnotationPlugin.js
client/src/HtmlSerializer.js
client/src/components/NoteInput.js
client/src/components/SlateEditor.js
client/yarn.lock
--- a/client/package.json	Wed Sep 05 13:48:10 2018 +0200
+++ b/client/package.json	Tue Sep 25 02:02:13 2018 +0200
@@ -5,6 +5,9 @@
   "homepage": ".",
   "dependencies": {
     "@types/react-modal": "^3.2.1",
+    "@types/slate-html-serializer": "^0.6.2",
+    "@types/slate-plain-serializer": "^0.5.1",
+    "@types/slate-react": "^0.18.0",
     "bootstrap": "^4.1.3",
     "i18next": "^11.6.0",
     "immutable": "^3.8.1",
@@ -27,7 +30,10 @@
     "redux-persist-immutable": "^4.3.0",
     "redux-persist-transform-immutable": "^4.3.0",
     "redux-saga": "^0.15.6",
-    "slate": "^0.20.1",
+    "slate": "^0.40.2",
+    "slate-html-serializer": "^0.7.2",
+    "slate-plain-serializer": "^0.6.2",
+    "slate-react": "^0.18.5",
     "uuid": "^3.0.1",
     "yarn": "^1.6.0"
   },
--- a/client/src/AnnotationPlugin.js	Wed Sep 05 13:48:10 2018 +0200
+++ b/client/src/AnnotationPlugin.js	Tue Sep 25 02:02:13 2018 +0200
@@ -3,13 +3,12 @@
   const { onChange } = options
 
   return {
-    onSelect(event, data, state, editor) {
+    onSelect(event, change) {
       event.preventDefault()
 
-      const selection = data.selection;
-
-      const startOffset = selection.startOffset;
-      const endOffset = selection.endOffset;
+      const { value } = change
+      const { selection } = value
+      const { start, end} = selection
 
       if (selection.isCollapsed) {
         return;
@@ -21,14 +20,14 @@
 
       // Keep only the relevant nodes,
       // i.e. nodes which are contained within selection
-      state.document.nodes.forEach((node) => {
-        if (selection.hasStartIn(node)) {
+      value.document.nodes.forEach((node) => {
+        if (start.isInNode(node)) {
           hasStarted = true;
         }
         if (hasStarted && !hasEnded) {
           nodes.push(node);
         }
-        if (selection.hasEndIn(node)) {
+        if (end.isAtEndOfNode(node)) {
           hasEnded = true;
         }
       });
@@ -37,17 +36,17 @@
 
       // Concatenate the nodes text
       if (nodes.length === 1) {
-        text = nodes[0].text.substring(startOffset, endOffset);
+        text = nodes[0].text.substring(start.offset, end.offset);
       } else {
         text = nodes.map((node) => {
-          if (selection.hasStartIn(node)) return node.text.substring(startOffset);
-          if (selection.hasEndIn(node)) return node.text.substring(0, endOffset);
+          if (start.isInNode(node)) return node.text.substring(start.offset);
+          if (end.isAtEndOfNode(node)) return node.text.substring(0, end.offset);
           return node.text;
         }).join('\n');
       }
 
       if (onChange) {
-        onChange(text, startOffset, endOffset);
+        onChange(text, start.offset, end.offset);
       }
     }
 
--- a/client/src/HtmlSerializer.js	Wed Sep 05 13:48:10 2018 +0200
+++ b/client/src/HtmlSerializer.js	Tue Sep 25 02:02:13 2018 +0200
@@ -1,15 +1,18 @@
 import React from 'react'
-import { Html } from 'slate'
+import Html from 'slate-html-serializer'
 
 const BLOCK_TAGS = {
   p: 'paragraph',
+  ul: 'bulleted-list',
+  ol: 'numbered-list',
+  li: 'list-item',
 }
 
 // Add a dictionary of mark tags.
 const MARK_TAGS = {
   em: 'italic',
   strong: 'bold',
-  u: 'underline',
+  u: 'underlined',
   category: 'span'
 }
 
@@ -20,16 +23,23 @@
       const type = BLOCK_TAGS[el.tagName]
       if (!type) return
       return {
-        kind: 'block',
+        object: 'block',
         type: type,
-        nodes: next(el.children)
+        nodes: next(el.childNodes)
       }
     },
-    serialize(object, children) {
-      if (object.kind !== 'block') return
-      switch (object.type) {
+    serialize(obj, children) {
+      if (obj.object !== 'block') return
+      switch (obj.type) {
+        case 'numbered-list':
+          return <ol>{children}</ol>;
+        case 'bulleted-list':
+          return <ul>{children}</ul>;
+         case 'list-item':
+          return <li>{children}</li>;
         case 'paragraph':
-        case 'line': return <p>{children}</p>
+        case 'line':
+          return <p>{children}</p>
         default: return;
       }
     }
@@ -40,18 +50,22 @@
       const type = MARK_TAGS[el.tagName]
       if (!type) return
       return {
-        kind: 'mark',
+        object: 'mark',
         type: type,
-        nodes: next(el.children)
+        nodes: next(el.childNodes)
       }
     },
-    serialize(object, children) {
-      if (object.kind !== 'mark') return
-      switch (object.type) {
-        case 'bold': return <strong>{children}</strong>
-        case 'italic': return <em>{children}</em>
-        case 'underline': return <u>{children}</u>
-        case 'category': return <span style={{ backgroundColor: object.data.get('color') }}>{children}</span>
+    serialize(obj, children) {
+      if (obj.object !== 'mark') return
+      switch (obj.type) {
+        case 'bold':
+          return <strong>{children}</strong>
+        case 'italic':
+          return <em>{children}</em>
+        case 'underlined':
+          return <ins>{children}</ins>
+        case 'category':
+          return <span style={{ backgroundColor: obj.data.get('color') }}>{children}</span>
         default: return;
       }
     }
--- a/client/src/components/NoteInput.js	Wed Sep 05 13:48:10 2018 +0200
+++ b/client/src/components/NoteInput.js	Tue Sep 25 02:02:13 2018 +0200
@@ -14,7 +14,7 @@
 
   onEditorChange = (e) => {
     this.setState({
-      buttonDisabled: e.state.document.length === 0,
+      buttonDisabled: e.value.document === 0,
       startedAt: e.startedAt,
       finishedAt: e.finishedAt
     });
@@ -26,7 +26,7 @@
     const raw = this.refs.editor.asRaw();
     const html = this.refs.editor.asHtml();
     const categories = this.refs.editor.asCategories();
-    const marginComment = this.marginComment.value;
+    // const marginComment = this.marginComment.value;
 
     this.props.addNote(this.props.session, {
       plain: plain,
@@ -35,7 +35,7 @@
       startedAt: this.state.startedAt,
       finishedAt: now(),
       categories: categories,
-      marginComment: marginComment,
+      // marginComment: marginComment,
     });
 
     this.refs.editor.clear();
@@ -54,8 +54,8 @@
   render() {
     return (
       <form>
-        <div className="editor">
-          <div className="editor-left">
+        <div className="editor p-3 mb-3">
+          <div className="editor-left w-100 border-0 pl-3 pb-3 sticky-bottom">
             <SlateEditor ref="editor"
               onChange={this.onEditorChange}
               onEnterKeyDown={this.onAddNoteClick}
@@ -64,15 +64,16 @@
               isChecked={this.props.autoSubmit}
               isButtonDisabled={this.state.buttonDisabled}
               annotationCategories={ this.props.annotationCategories } />
+
           </div>
-          <div className="editor-right">
-            <input type="textarea" className="form-control"
+          {/* <div className="editor-right w-25 pl-2 border-0 sticky-bottom">
+            <input type="textarea" className="form-control h-100"
               name="margin"
-              placeholder="Enter a margin comment for your note"
+              placeholder="Espace de commentaire pour votre note"
               // inputRef={ ref => { this.marginComment = ref; } }
               ref={(marginComment) => { this.marginComment = marginComment; }}
             />
-          </div>
+          </div> */}
         </div>
       </form>
     );
--- 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>
     )
   }
 
--- a/client/yarn.lock	Wed Sep 05 13:48:10 2018 +0200
+++ b/client/yarn.lock	Tue Sep 25 02:02:13 2018 +0200
@@ -28,6 +28,33 @@
     "@types/prop-types" "*"
     csstype "^2.2.0"
 
+"@types/slate-html-serializer@^0.6.2":
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/@types/slate-html-serializer/-/slate-html-serializer-0.6.2.tgz#7e4bf258bb52489cd392a8f26c2fe04eb97c7cfc"
+  dependencies:
+    "@types/react" "*"
+    "@types/slate" "*"
+
+"@types/slate-plain-serializer@^0.5.1":
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/@types/slate-plain-serializer/-/slate-plain-serializer-0.5.1.tgz#69f27bafbfc7c00739470c1ac4a066c42f6908ce"
+  dependencies:
+    "@types/slate" "*"
+
+"@types/slate-react@^0.18.0":
+  version "0.18.0"
+  resolved "https://registry.yarnpkg.com/@types/slate-react/-/slate-react-0.18.0.tgz#f883a161888dd9bc18db23c4d1bd66b7ae9a2bb7"
+  dependencies:
+    "@types/react" "*"
+    "@types/slate" "*"
+    immutable "^3.8.2"
+
+"@types/slate@*":
+  version "0.40.0"
+  resolved "https://registry.yarnpkg.com/@types/slate/-/slate-0.40.0.tgz#cc1288bd3d7ba56444fecbab5a7c292d101d263f"
+  dependencies:
+    immutable "^3.8.2"
+
 JSONStream@^1.3.2:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.4.tgz#615bb2adb0cd34c8f4c447b5f6512fa1d8f16a2e"
@@ -1203,10 +1230,6 @@
   version "4.1.3"
   resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
 
-bootstrap@^4.1.3:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
-
 boxen@^1.2.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
@@ -2185,7 +2208,7 @@
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
 
-debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.2, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
+debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
@@ -2593,7 +2616,7 @@
     es5-ext "^0.10.35"
     es6-symbol "^3.1.1"
 
-es6-map@^0.1.3, es6-map@^0.1.4:
+es6-map@^0.1.3:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
   dependencies:
@@ -3908,7 +3931,7 @@
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
 
-immutable@^3.8.1:
+immutable@^3.8.1, immutable@^3.8.2:
   version "3.8.2"
   resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
 
@@ -4139,10 +4162,6 @@
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
 
-is-empty@^1.0.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/is-empty/-/is-empty-1.2.0.tgz#de9bb5b278738a05a0b09a57e1fb4d4a341a9f6b"
-
 is-equal-shallow@^0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
@@ -4201,6 +4220,10 @@
   dependencies:
     is-extglob "^2.1.1"
 
+is-hotkey@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.3.tgz#8a129eec16f3941bd4f37191e02b9c3e91950549"
+
 is-in-browser@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
@@ -4374,6 +4397,10 @@
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
 
+isomorphic-base64@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/isomorphic-base64/-/isomorphic-base64-1.0.2.tgz#f426aae82569ba8a4ec5ca73ad21a44ab1ee7803"
+
 isomorphic-fetch@^2.1.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
@@ -4811,10 +4838,6 @@
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
 
-keycode@^2.1.2:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
-
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -5114,6 +5137,10 @@
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
+lodash@^4.1.1:
+  version "4.17.11"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+
 longest@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -5257,6 +5284,10 @@
   dependencies:
     mimic-fn "^1.0.0"
 
+memoize-one@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.2.tgz#3fb8db695aa14ab9c0f1644e1585a8806adc1aee"
+
 memory-fs@^0.4.0, memory-fs@~0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@@ -7085,6 +7116,10 @@
     settle-promise "1.0.0"
     source-map "0.5.6"
 
+react-immutable-proptypes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4"
+
 react-is@^16.3.2:
   version "16.4.2"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88"
@@ -7943,24 +7978,72 @@
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
 
-slate@^0.20.1:
-  version "0.20.7"
-  resolved "https://registry.yarnpkg.com/slate/-/slate-0.20.7.tgz#083ca9074dc7fd3ad8863985e6d92ed76bdc9eff"
-  dependencies:
-    cheerio "^0.22.0"
-    debug "^2.3.2"
+slate-base64-serializer@^0.2.63:
+  version "0.2.63"
+  resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.63.tgz#b086dfce5145c29b8465dc54ff5493b726ddca07"
+  dependencies:
+    isomorphic-base64 "^1.0.2"
+
+slate-dev-environment@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/slate-dev-environment/-/slate-dev-environment-0.2.0.tgz#c43f4a5e13cccc16ad4c3015c8ad182b6e5b5b3a"
+  dependencies:
+    is-in-browser "^1.1.3"
+
+slate-dev-warning@^0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/slate-dev-warning/-/slate-dev-warning-0.0.1.tgz#f6c36731babea5e301b5bd504fe64911dd24200a"
+
+slate-hotkeys@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/slate-hotkeys/-/slate-hotkeys-0.2.3.tgz#843a467421c643b4a1a3c240957c8adcfa99deb3"
+  dependencies:
+    is-hotkey "^0.1.3"
+    slate-dev-environment "^0.2.0"
+
+slate-html-serializer@^0.7.2:
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/slate-html-serializer/-/slate-html-serializer-0.7.2.tgz#8b47cc24ece9b99be0d73bd5d55aeb3a66db73cb"
+  dependencies:
+    type-of "^2.0.1"
+
+slate-plain-serializer@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.6.2.tgz#8d406f9523d7ba219d14bf300d30d7e093430247"
+
+slate-prop-types@^0.4.61:
+  version "0.4.61"
+  resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.61.tgz#141c109bed81b130dd03ab86dd7541b28d6d962a"
+
+slate-react@^0.18.5:
+  version "0.18.5"
+  resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.18.5.tgz#089ae30cd19b7600a9b528b5ab0525038b500142"
+  dependencies:
+    debug "^3.1.0"
+    get-window "^1.1.1"
+    is-window "^1.0.2"
+    lodash "^4.1.1"
+    memoize-one "^4.0.0"
+    prop-types "^15.5.8"
+    react-immutable-proptypes "^2.1.0"
+    selection-is-backward "^1.0.0"
+    slate-base64-serializer "^0.2.63"
+    slate-dev-environment "^0.2.0"
+    slate-dev-warning "^0.0.1"
+    slate-hotkeys "^0.2.3"
+    slate-plain-serializer "^0.6.2"
+    slate-prop-types "^0.4.61"
+
+slate@^0.40.2:
+  version "0.40.2"
+  resolved "https://registry.yarnpkg.com/slate/-/slate-0.40.2.tgz#3adbd4bb66c16208b2dc3f0900b1857ea7912cb0"
+  dependencies:
+    debug "^3.1.0"
     direction "^0.1.5"
-    es6-map "^0.1.4"
     esrever "^0.2.0"
-    get-window "^1.1.1"
-    immutable "^3.8.1"
-    is-empty "^1.0.0"
-    is-in-browser "^1.1.3"
-    is-window "^1.0.2"
-    keycode "^2.1.2"
-    prop-types "^15.5.8"
-    react-portal "^3.1.0"
-    selection-is-backward "^1.0.0"
+    is-plain-object "^2.0.4"
+    lodash "^4.17.4"
+    slate-dev-warning "^0.0.1"
     type-of "^2.0.1"
 
 slice-ansi@0.0.4: