Introduce "annotation" plugin.
authorAlexandre Segura <mex.zktk@gmail.com>
Tue, 06 Jun 2017 15:56:41 +0200
changeset 19 f1b125b95fe9
parent 18 dab2a16500e0
child 20 a8300ef1876e
Introduce "annotation" plugin. - Wrap mark around text. - Store selected text in mark data. - Colorize selected text.
client/src/AnnotationPlugin.js
client/src/App.scss
client/src/HtmlSerializer.js
client/src/components/NotesList.js
client/src/components/SlateEditor.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/AnnotationPlugin.js	Tue Jun 06 15:56:41 2017 +0200
@@ -0,0 +1,57 @@
+function AnnotationPlugin(options) {
+
+  const { onChange } = options
+
+  return {
+    onSelect(event, data, state, editor) {
+      event.preventDefault()
+
+      const selection = data.selection;
+
+      const startOffset = selection.startOffset;
+      const endOffset = selection.endOffset;
+
+      if (selection.isCollapsed) {
+        return;
+      }
+
+      let nodes = [];
+      let hasStarted = false;
+      let hasEnded = false;
+
+      // Keep only the relevant nodes,
+      // i.e. nodes which are contained within selection
+      state.document.nodes.forEach((node) => {
+        if (selection.hasStartIn(node)) {
+          hasStarted = true;
+        }
+        if (hasStarted && !hasEnded) {
+          nodes.push(node);
+        }
+        if (selection.hasEndIn(node)) {
+          hasEnded = true;
+        }
+      });
+
+      let text = '';
+
+      // Concatenate the nodes text
+      if (nodes.length === 1) {
+        text = nodes[0].text.substring(startOffset, endOffset);
+      } else {
+        text = nodes.map((node) => {
+          if (selection.hasStartIn(node)) return node.text.substring(startOffset);
+          if (selection.hasEndIn(node)) return node.text.substring(0, endOffset);
+          return node.text;
+        }).join('\n');
+      }
+
+      if (onChange) {
+        onChange(text);
+      }
+    }
+
+  };
+}
+
+export default AnnotationPlugin;
--- a/client/src/App.scss	Thu Jun 01 19:01:03 2017 +0200
+++ b/client/src/App.scss	Tue Jun 06 15:56:41 2017 +0200
@@ -1,3 +1,5 @@
+@import "bootstrap/variables";
+
 .App {
   text-align: center;
 }
@@ -19,13 +21,13 @@
 }
 
 .toolbar-menu {
-  padding: 1px 0 17px 18px;
-  // margin: 0 -20px;
+  padding-bottom: 10px;
   border-bottom: 2px solid #eee;
   margin-bottom: 20px;
   .button {
     color: #ccc;
     cursor: pointer;
+    margin-right: 10px;
   }
 
   .button[data-active="true"] {
@@ -48,6 +50,7 @@
     position: relative;
     padding-left: 70px;
     margin-bottom: 20px;
+    min-height: ($line-height-computed * 3);
 
     &:before {
         content: "";
@@ -75,3 +78,10 @@
         left: 0;
     }
 }
+
+span.annotation {
+    background-color: yellow;
+    text-decoration-line: underline;
+    text-decoration-style: dotted;
+
+}
--- a/client/src/HtmlSerializer.js	Thu Jun 01 19:01:03 2017 +0200
+++ b/client/src/HtmlSerializer.js	Tue Jun 06 15:56:41 2017 +0200
@@ -2,9 +2,7 @@
 import { Html } from 'slate'
 
 const BLOCK_TAGS = {
-  blockquote: 'quote',
   p: 'paragraph',
-  pre: 'code'
 }
 
 // Add a dictionary of mark tags.
@@ -12,6 +10,13 @@
   em: 'italic',
   strong: 'bold',
   u: 'underline',
+  annotation: 'span'
+}
+
+const annotationStyle = {
+  textDecoration: 'underline',
+  textDecorationStyle: 'dotted',
+  backgroundColor: 'yellow'
 }
 
 const rules = [
@@ -27,12 +32,11 @@
       }
     },
     serialize(object, children) {
-      if (object.kind != 'block') return
+      if (object.kind !== 'block') return
       switch (object.type) {
-        case 'code': return <pre><code>{children}</code></pre>
         case 'paragraph':
         case 'line': return <p>{children}</p>
-        case 'quote': return <blockquote>{children}</blockquote>
+        default: return;
       }
     }
   },
@@ -48,11 +52,13 @@
       }
     },
     serialize(object, children) {
-      if (object.kind != 'mark') return
+      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 'annotation': return <span style={annotationStyle}>{children}</span>
+        default: return;
       }
     }
   }
--- a/client/src/components/NotesList.js	Thu Jun 01 19:01:03 2017 +0200
+++ b/client/src/components/NotesList.js	Tue Jun 06 15:56:41 2017 +0200
@@ -3,7 +3,7 @@
 import PropTypes from 'prop-types';
 import Immutable from 'immutable';
 
-import { ListGroup, ListGroupItem, Alert } from 'react-bootstrap';
+import { Alert } from 'react-bootstrap';
 
 import Note from './Note';
 
@@ -18,7 +18,7 @@
   return (
     <div>
       {notes.map((note) =>
-        <Note note={note} />
+        <Note note={note} key={note.id} />
       )}
     </div>
   );
--- a/client/src/components/SlateEditor.js	Thu Jun 01 19:01:03 2017 +0200
+++ b/client/src/components/SlateEditor.js	Tue Jun 06 15:56:41 2017 +0200
@@ -2,6 +2,9 @@
 import React from 'react'
 import moment from 'moment';
 import HtmlSerializer from '../HtmlSerializer'
+import AnnotationPlugin from '../AnnotationPlugin';
+
+const plugins = [];
 
 /**
  * Define the default node type.
@@ -17,10 +20,7 @@
 
 const schema = {
   nodes: {
-    'block-quote': props => <blockquote {...props.attributes}>{props.children}</blockquote>,
     'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
-    'heading-one': props => <h1 {...props.attributes}>{props.children}</h1>,
-    'heading-two': props => <h2 {...props.attributes}>{props.children}</h2>,
     'list-item': props => <li {...props.attributes}>{props.children}</li>,
     'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
   },
@@ -28,11 +28,12 @@
     bold: {
       fontWeight: 'bold'
     },
-    code: {
-      fontFamily: 'monospace',
-      backgroundColor: '#eee',
-      padding: '3px',
-      borderRadius: '4px'
+    // TODO Check if we can move this to the plugin using the schema option
+    // https://docs.slatejs.org/reference/plugins/plugin.html#schema
+    annotation: {
+      textDecoration: 'underline',
+      textDecorationStyle: 'dotted',
+      backgroundColor: 'yellow',
     },
     italic: {
       fontStyle: 'italic'
@@ -58,10 +59,20 @@
    */
   constructor(props) {
     super(props);
+
+    const annotationPlugin = AnnotationPlugin({
+      onChange: (text) => {
+        this.setState({ currentSelectionText: text });
+      }
+    });
+
+    plugins.push(annotationPlugin);
+
     this.state = {
       state: Plain.deserialize(''),
       startedAt: null,
-      finishedAt: null
+      finishedAt: null,
+      currentSelectionText: ''
     };
   }
 
@@ -174,9 +185,6 @@
       case 'u':
         mark = 'underlined'
         break
-      case '`':
-        mark = 'code'
-        break
       default:
         return
     }
@@ -201,9 +209,16 @@
     e.preventDefault()
     let { state } = this.state
 
+    let toggleMarkOptions;
+    if (type === 'annotation') {
+      toggleMarkOptions = { type: type, data: { text: this.state.currentSelectionText } }
+    } else {
+      toggleMarkOptions = type;
+    }
+
     state = state
       .transform()
-      .toggleMark(type)
+      .toggleMark(toggleMarkOptions)
       .apply()
 
     this.setState({ state })
@@ -294,10 +309,8 @@
         {this.renderMarkButton('bold', 'format_bold')}
         {this.renderMarkButton('italic', 'format_italic')}
         {this.renderMarkButton('underlined', 'format_underlined')}
-        {this.renderMarkButton('code', 'code')}
-        {this.renderBlockButton('heading-one', 'looks_one')}
-        {this.renderBlockButton('heading-two', 'looks_two')}
-        {this.renderBlockButton('block-quote', 'format_quote')}
+        {this.renderMarkButton('annotation', 'label')}
+
         {this.renderBlockButton('numbered-list', 'format_list_numbered')}
         {this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
       </div>
@@ -356,6 +369,7 @@
           spellCheck
           placeholder={'Enter some rich text...'}
           schema={schema}
+          plugins={plugins}
           state={this.state.state}
           onChange={this.onChange}
           onKeyDown={this.onKeyDown}