client/src/components/SlateEditor.js
changeset 5 5c91bfa8fcde
child 8 6f572b6b6be3
equal deleted inserted replaced
4:885a20cde527 5:5c91bfa8fcde
       
     1 import { Editor, Raw, Plain } from 'slate'
       
     2 import React from 'react'
       
     3 import initialState from './state.json'
       
     4 
       
     5 /**
       
     6  * Define the default node type.
       
     7  */
       
     8 
       
     9 const DEFAULT_NODE = 'paragraph'
       
    10 
       
    11 /**
       
    12  * Define a schema.
       
    13  *
       
    14  * @type {Object}
       
    15  */
       
    16 
       
    17 const schema = {
       
    18   nodes: {
       
    19     'block-quote': props => <blockquote {...props.attributes}>{props.children}</blockquote>,
       
    20     'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
       
    21     'heading-one': props => <h1 {...props.attributes}>{props.children}</h1>,
       
    22     'heading-two': props => <h2 {...props.attributes}>{props.children}</h2>,
       
    23     'list-item': props => <li {...props.attributes}>{props.children}</li>,
       
    24     'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
       
    25   },
       
    26   marks: {
       
    27     bold: {
       
    28       fontWeight: 'bold'
       
    29     },
       
    30     code: {
       
    31       fontFamily: 'monospace',
       
    32       backgroundColor: '#eee',
       
    33       padding: '3px',
       
    34       borderRadius: '4px'
       
    35     },
       
    36     italic: {
       
    37       fontStyle: 'italic'
       
    38     },
       
    39     underlined: {
       
    40       textDecoration: 'underline'
       
    41     }
       
    42   }
       
    43 }
       
    44 
       
    45 /**
       
    46  * The rich text example.
       
    47  *
       
    48  * @type {Component}
       
    49  */
       
    50 
       
    51 class RichText extends React.Component {
       
    52 
       
    53   /**
       
    54    * Deserialize the initial editor state.
       
    55    *
       
    56    * @type {Object}
       
    57    */
       
    58 
       
    59   state = {
       
    60     state: Raw.deserialize(initialState, { terse: true })
       
    61   };
       
    62 
       
    63   /**
       
    64    * Check if the current selection has a mark with `type` in it.
       
    65    *
       
    66    * @param {String} type
       
    67    * @return {Boolean}
       
    68    */
       
    69 
       
    70   hasMark = (type) => {
       
    71     const { state } = this.state
       
    72     return state.marks.some(mark => mark.type === type)
       
    73   }
       
    74 
       
    75   /**
       
    76    * Check if the any of the currently selected blocks are of `type`.
       
    77    *
       
    78    * @param {String} type
       
    79    * @return {Boolean}
       
    80    */
       
    81 
       
    82   hasBlock = (type) => {
       
    83     const { state } = this.state
       
    84     return state.blocks.some(node => node.type === type)
       
    85   }
       
    86 
       
    87   /**
       
    88    * On change, save the new state.
       
    89    *
       
    90    * @param {State} state
       
    91    */
       
    92 
       
    93   onChange = (state) => {
       
    94     this.setState({ state })
       
    95 
       
    96   }
       
    97 
       
    98   asPlain = () => {
       
    99     return Plain.serialize(this.state.state);
       
   100   }
       
   101 
       
   102   clear = () => {
       
   103     const state = Plain.deserialize('');
       
   104     this.setState({ stateĀ });
       
   105   }
       
   106 
       
   107   /**
       
   108    * On key down, if it's a formatting command toggle a mark.
       
   109    *
       
   110    * @param {Event} e
       
   111    * @param {Object} data
       
   112    * @param {State} state
       
   113    * @return {State}
       
   114    */
       
   115 
       
   116   onKeyDown = (e, data, state) => {
       
   117     if (!data.isMod) return
       
   118     let mark
       
   119 
       
   120     switch (data.key) {
       
   121       case 'b':
       
   122         mark = 'bold'
       
   123         break
       
   124       case 'i':
       
   125         mark = 'italic'
       
   126         break
       
   127       case 'u':
       
   128         mark = 'underlined'
       
   129         break
       
   130       case '`':
       
   131         mark = 'code'
       
   132         break
       
   133       default:
       
   134         return
       
   135     }
       
   136 
       
   137     state = state
       
   138       .transform()
       
   139       .toggleMark(mark)
       
   140       .apply()
       
   141 
       
   142     e.preventDefault()
       
   143     return state
       
   144   }
       
   145 
       
   146   /**
       
   147    * When a mark button is clicked, toggle the current mark.
       
   148    *
       
   149    * @param {Event} e
       
   150    * @param {String} type
       
   151    */
       
   152 
       
   153   onClickMark = (e, type) => {
       
   154     e.preventDefault()
       
   155     let { state } = this.state
       
   156 
       
   157     state = state
       
   158       .transform()
       
   159       .toggleMark(type)
       
   160       .apply()
       
   161 
       
   162     this.setState({ state })
       
   163   }
       
   164 
       
   165   /**
       
   166    * When a block button is clicked, toggle the block type.
       
   167    *
       
   168    * @param {Event} e
       
   169    * @param {String} type
       
   170    */
       
   171 
       
   172   onClickBlock = (e, type) => {
       
   173     e.preventDefault()
       
   174     let { state } = this.state
       
   175     const transform = state.transform()
       
   176     const { document } = state
       
   177 
       
   178     // Handle everything but list buttons.
       
   179     if (type !== 'bulleted-list' && type !== 'numbered-list') {
       
   180       const isActive = this.hasBlock(type)
       
   181       const isList = this.hasBlock('list-item')
       
   182 
       
   183       if (isList) {
       
   184         transform
       
   185           .setBlock(isActive ? DEFAULT_NODE : type)
       
   186           .unwrapBlock('bulleted-list')
       
   187           .unwrapBlock('numbered-list')
       
   188       }
       
   189 
       
   190       else {
       
   191         transform
       
   192           .setBlock(isActive ? DEFAULT_NODE : type)
       
   193       }
       
   194     }
       
   195 
       
   196     // Handle the extra wrapping required for list buttons.
       
   197     else {
       
   198       const isList = this.hasBlock('list-item')
       
   199       const isType = state.blocks.some((block) => {
       
   200         return !!document.getClosest(block.key, parent => parent.type === type)
       
   201       })
       
   202 
       
   203       if (isList && isType) {
       
   204         transform
       
   205           .setBlock(DEFAULT_NODE)
       
   206           .unwrapBlock('bulleted-list')
       
   207           .unwrapBlock('numbered-list')
       
   208       } else if (isList) {
       
   209         transform
       
   210           .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list')
       
   211           .wrapBlock(type)
       
   212       } else {
       
   213         transform
       
   214           .setBlock('list-item')
       
   215           .wrapBlock(type)
       
   216       }
       
   217     }
       
   218 
       
   219     state = transform.apply()
       
   220     this.setState({ state })
       
   221   }
       
   222 
       
   223   /**
       
   224    * Render.
       
   225    *
       
   226    * @return {Element}
       
   227    */
       
   228 
       
   229   render = () => {
       
   230     return (
       
   231       <div>
       
   232         {this.renderToolbar()}
       
   233         {this.renderEditor()}
       
   234       </div>
       
   235     )
       
   236   }
       
   237 
       
   238   /**
       
   239    * Render the toolbar.
       
   240    *
       
   241    * @return {Element}
       
   242    */
       
   243 
       
   244   renderToolbar = () => {
       
   245     return (
       
   246       <div className="menu toolbar-menu">
       
   247         {this.renderMarkButton('bold', 'format_bold')}
       
   248         {this.renderMarkButton('italic', 'format_italic')}
       
   249         {this.renderMarkButton('underlined', 'format_underlined')}
       
   250         {this.renderMarkButton('code', 'code')}
       
   251         {this.renderBlockButton('heading-one', 'looks_one')}
       
   252         {this.renderBlockButton('heading-two', 'looks_two')}
       
   253         {this.renderBlockButton('block-quote', 'format_quote')}
       
   254         {this.renderBlockButton('numbered-list', 'format_list_numbered')}
       
   255         {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
       
   256       </div>
       
   257     )
       
   258   }
       
   259 
       
   260   /**
       
   261    * Render a mark-toggling toolbar button.
       
   262    *
       
   263    * @param {String} type
       
   264    * @param {String} icon
       
   265    * @return {Element}
       
   266    */
       
   267 
       
   268   renderMarkButton = (type, icon) => {
       
   269     const isActive = this.hasMark(type)
       
   270     const onMouseDown = e => this.onClickMark(e, type)
       
   271 
       
   272     return (
       
   273       <span className="button" onMouseDown={onMouseDown} data-active={isActive}>
       
   274         <span className="material-icons">{icon}</span>
       
   275       </span>
       
   276     )
       
   277   }
       
   278 
       
   279   /**
       
   280    * Render a block-toggling toolbar button.
       
   281    *
       
   282    * @param {String} type
       
   283    * @param {String} icon
       
   284    * @return {Element}
       
   285    */
       
   286 
       
   287   renderBlockButton = (type, icon) => {
       
   288     const isActive = this.hasBlock(type)
       
   289     const onMouseDown = e => this.onClickBlock(e, type)
       
   290 
       
   291     return (
       
   292       <span className="button" onMouseDown={onMouseDown} data-active={isActive}>
       
   293         <span className="material-icons">{icon}</span>
       
   294       </span>
       
   295     )
       
   296   }
       
   297 
       
   298   /**
       
   299    * Render the Slate editor.
       
   300    *
       
   301    * @return {Element}
       
   302    */
       
   303 
       
   304   renderEditor = () => {
       
   305     return (
       
   306       <div className="editor">
       
   307         <Editor
       
   308           spellCheck
       
   309           placeholder={'Enter some rich text...'}
       
   310           schema={schema}
       
   311           state={this.state.state}
       
   312           onChange={this.onChange}
       
   313           onKeyDown={this.onKeyDown}
       
   314         />
       
   315       </div>
       
   316     )
       
   317   }
       
   318 
       
   319 }
       
   320 
       
   321 /**
       
   322  * Export.
       
   323  */
       
   324 
       
   325 export default RichText