From d64c7117dde419e53561e7580dbf09e51fbd7f13 Mon Sep 17 00:00:00 2001
From: Tim <roadscape@users.noreply.github.com>
Date: Mon, 10 Oct 2016 14:10:49 -0400
Subject: [PATCH] image drag-drop #185

---
 app/components/elements/SlateEditor.jsx  |  32 ++++--
 app/components/elements/SlateEditor.scss |  40 ++++---
 app/utils/SlateEditor/Image.js           |  45 ++++++++
 app/utils/SlateEditor/Schema.js          | 134 +++++++++++++----------
 package.json                             |   1 +
 5 files changed, 170 insertions(+), 82 deletions(-)
 create mode 100644 app/utils/SlateEditor/Image.js

diff --git a/app/components/elements/SlateEditor.jsx b/app/components/elements/SlateEditor.jsx
index f2e97f6af..a96256a09 100644
--- a/app/components/elements/SlateEditor.jsx
+++ b/app/components/elements/SlateEditor.jsx
@@ -11,7 +11,25 @@ export const serializeHtml   = (state) => serializer.serialize(state)
 export const deserializeHtml = (html)  => serializer.deserialize(html)
 export const getDemoState    = ()      => Raw.deserialize(demoState, { terse: true })
 
-const plugins = []
+let plugins = []
+
+if(process.env.BROWSER) {
+    //import InsertImages from 'slate-drop-or-paste-images'
+    const InsertImages = require('slate-drop-or-paste-images').default
+
+    plugins.push(
+        InsertImages({
+            extensions: ['jpeg'],
+            applyTransform: (transform, file) => {
+                return transform.insertBlock({
+                    type: 'image',
+                    isVoid: true,
+                    data: { file }
+                })
+            }
+        })
+    )
+}
 
 
 export default class SlateEditor extends React.Component {
@@ -138,7 +156,6 @@ export default class SlateEditor extends React.Component {
             .apply()
     }
 
-
     render = () => {
         const { state } = this.state
         return (
@@ -155,22 +172,19 @@ export default class SlateEditor extends React.Component {
         return (
             <Portal isOpened onOpen={this.onOpen}>
                 <div className="SlateEditor__menu SlateEditor__hover-menu">
-                    {this.renderMarkButton('bold',      <strong>B</strong>)}
-                    {this.renderMarkButton('italic',    <i>I</i>)}
-                    {this.renderMarkButton('underline', <u>U</u>)}
-                    {this.renderMarkButton('strike',    <del>S</del>)}
-                    {this.renderMarkButton('code',      <code>{'{}'}</code>)}
+                    {schema.toolbarMarks.map(this.renderMarkButton)}
                 </div>
             </Portal>
         )
     }
 
-    renderMarkButton = (type, label) => {
+    renderMarkButton = (props) => {
+        const {type, label} = props
         const isActive = this.hasMark(type)
         const onMouseDown = e => this.onClickMark(e, type)
 
         return (
-            <span className="SlateEditor__menu-button" onMouseDown={onMouseDown} data-active={isActive}>
+            <span key={type} className="SlateEditor__menu-button" onMouseDown={onMouseDown} data-active={isActive}>
                 <span>{label}</span>
             </span>
         )
diff --git a/app/components/elements/SlateEditor.scss b/app/components/elements/SlateEditor.scss
index 3d3cc2dbc..4edf52744 100644
--- a/app/components/elements/SlateEditor.scss
+++ b/app/components/elements/SlateEditor.scss
@@ -6,6 +6,7 @@
 
 .SlateEditor.Markdown {
   a {border-bottom: 1px dotted #00f;}
+  img.active {box-shadow: 0 0 0 2px blue;}
 }
 
 .SlateEditor > * > * + * {
@@ -17,7 +18,7 @@
 }
 
 .SlateEditor__menu > * + * {
-  margin-left: 4px;
+  margin: 0;
 }
 
 .SlateEditor__menu-button {
@@ -26,28 +27,26 @@
   cursor: pointer;
   > span {
     display: inline-block;
-    width: 1.5rem;
+    width: 2rem;
     text-align: center;
-    background: #555;
+    background: rgba(0,0,0,0.1);
     border-radius: 2px;
     code {
       border: none;
       background: transparent;
       color: inherit;
+      padding: 0;
+      font-size: 90%;
+      vertical-align: top;
     }
   }
 }
 
-.SlateEditor__menu-button[data-active="false"] {
-  > span:hover {
-    color: #333;
-    background: #CCC;
-  }
-}
+.SlateEditor__menu-button[data-active="false"]:hover,
 .SlateEditor__menu-button[data-active="true"] {
   > span {
-    color: #333;
-    background: #EEE;
+    color: #32cd32;
+    background: rgba(0,0,0,0.5);
   }
 }
 
@@ -61,8 +60,8 @@
 */
 
 .SlateEditor__hover-menu {
-  font-size: 90%;
-  padding: 5px;
+  font-size: 110%;
+  padding: 1px;
   position: absolute;
   z-index: 1;
   top: -10000px;
@@ -72,4 +71,19 @@
   background-color: #222;
   border-radius: 4px;
   transition: opacity .75s;
+  background-image: linear-gradient(180deg,#464646,#151515);
+}
+
+.SlateEditor__hover-menu:after {
+    top: 100%;
+    left: 50%;
+    border: solid transparent;
+    content: " ";
+    height: 0;
+    width: 0;
+    position: absolute;
+    pointer-events: none;
+    border-top-color: #151515;
+    border-width: 5px;
+    margin-left: -5px;
 }
diff --git a/app/utils/SlateEditor/Image.js b/app/utils/SlateEditor/Image.js
new file mode 100644
index 000000000..2223c9145
--- /dev/null
+++ b/app/utils/SlateEditor/Image.js
@@ -0,0 +1,45 @@
+import React from 'react'
+
+export default class Image extends React.Component {
+    state = {};
+
+    componentDidMount() {
+        console.log("** image mounted..", this.state)
+        const { node } = this.props
+        const { data } = node
+        const file = data.get('file')
+        if(file) this.load(file)
+    }
+
+    load(file) {
+        console.log("** image being loaded.. ----->", file)
+        const reader = new FileReader()
+        reader.addEventListener('load', () => this.setState({ src: reader.result }))
+        reader.readAsDataURL(file)
+    }
+
+    render() {
+        const { node, state, attributes } = this.props
+        let { src } = this.state
+
+        const isFocused = state.selection.hasEdgeIn(node)
+        const className = isFocused ? 'active' : null
+
+        if(src) {
+            console.log("** uploaded image being rendered..", src, state)
+        } else {
+            const src2 = node.data.get('src')
+            console.log("** image source was not in state. data.src = ", src2)
+            src = src2;
+        }
+
+        if(!src) {
+            // src = 'https://img1.steemit.com/0x0/http://ariasprado.name/wp-content/uploads/2012/09/missing-tile-256x256.png'
+            // src = $STM_Config.img_proxy_prefix + '0x0/' + src
+        }
+
+        return src
+          ? <img {...attributes} src={src} className={className} />
+          : <span>Loading... ({src})</span>
+    }
+}
diff --git a/app/utils/SlateEditor/Schema.js b/app/utils/SlateEditor/Schema.js
index 162a9ae56..38b95d242 100644
--- a/app/utils/SlateEditor/Schema.js
+++ b/app/utils/SlateEditor/Schema.js
@@ -1,4 +1,5 @@
 import React from 'react'
+import Image from 'app/utils/SlateEditor/Image'
 
 /*
 
@@ -21,6 +22,7 @@ img
 
 */
 
+// Map html --> block type
 const BLOCK_TAGS = {
     blockquote: 'block-quote',
     p:          'paragraph',
@@ -34,6 +36,7 @@ const BLOCK_TAGS = {
     li:         'bulleted-list-item',
 }
 
+// Map HTML --> mark type
 const MARK_TAGS = {
     em:     'italic',
     i:      'italic',
@@ -43,11 +46,8 @@ const MARK_TAGS = {
     del:    'strike',
     strike: 'strike',
     code:   'code',
-/*
     sup:    'sup',
     sub:    'sub',
-
-*/
 }
 
 
@@ -56,57 +56,58 @@ export const HtmlRules = [
     // Block rules
     {
         deserialize: (el, next) => {
-			let type = BLOCK_TAGS[el.tagName]
-			if (!type) return
+            let type = BLOCK_TAGS[el.tagName]
+            if (!type) return
             if(type == 'bulleted-list-item' && el.parent.name == 'ol') type = 'numbered-list-item'
-			return {
-				kind: 'block',
-				type: type,
-				nodes: next(el.children)
-			}
+            return {
+                kind: 'block',
+                type: type,
+                nodes: next(el.children)
+            }
         },
         serialize: (object, children) => {
             if(object.kind !== 'block') return
-			switch(object.type) {
+            switch(object.type) {
                 case 'code':               return <pre><code>{children}</code></pre>
-				case 'paragraph':          return <p>{children}</p>
-				case 'block-quote':        return <blockquote>{children}</blockquote>
-				case 'bulleted-list':      return <ul>{children}</ul>
-				case 'numbered-list':      return <ol>{children}</ol>
-				case 'heading-one':        return <h1>{children}</h1>
-				case 'heading-two':        return <h2>{children}</h2>
-				case 'heading-three':      return <h3>{children}</h3>
-				case 'heading-four':       return <h4>{children}</h4>
-				case 'bulleted-list-item': return <li>{children}</li>
-				case 'numbered-list-item': return <li>{children}</li>
-			}
+                case 'paragraph':          return <p>{children}</p>
+                case 'block-quote':        return <blockquote>{children}</blockquote>
+                case 'bulleted-list':      return <ul>{children}</ul>
+                case 'numbered-list':      return <ol>{children}</ol>
+                case 'heading-one':        return <h1>{children}</h1>
+                case 'heading-two':        return <h2>{children}</h2>
+                case 'heading-three':      return <h3>{children}</h3>
+                case 'heading-four':       return <h4>{children}</h4>
+                case 'bulleted-list-item': return <li>{children}</li>
+                case 'numbered-list-item': return <li>{children}</li>
+            }
         }
     },
 
     // Mark rules
     {
         deserialize: (el, next) => {
-			const type = MARK_TAGS[el.tagName]
-			if (!type) return
-			return {
-				kind: 'mark',
-				type: type,
-				nodes: next(el.children)
-			}
+            const type = MARK_TAGS[el.tagName]
+            if (!type) return
+            return {
+                kind: 'mark',
+                type: type,
+                nodes: next(el.children)
+            }
         },
         serialize: (object, children) => {
             if(object.kind !== 'mark') return;
-			switch(object.type) {
-				case 'bold':      return <strong>{children}</strong>
-				case 'italic':    return <i>{children}</i>
-				case 'underline': return <u>{children}</u>
-				case 'strike':    return <del>{children}</del>
-				case 'code':      return <code>{children}</code>
-			}
+            switch(object.type) {
+                case 'bold':      return <strong>{children}</strong>
+                case 'italic':    return <i>{children}</i>
+                case 'underline': return <u>{children}</u>
+                case 'strike':    return <del>{children}</del>
+                case 'code':      return <code>{children}</code>
+                case 'sup':       return <sup>{children}</sup>
+                case 'sub':       return <sub>{children}</sub>
+            }
         }
     },
 
-
     // Custom
     {
         deserialize: (el, next) => {
@@ -121,51 +122,65 @@ export const HtmlRules = [
                 return {
                     kind: 'block',
                     type: 'image',
+                    isVoid: true,
                     data: {src: el.attribs.src},
                     nodes: next(el.children)
                 }
             }
             if (el.tagName == 'a') {
-                console.log("deserialized <a>, the href is", el.attribs.href)
+                const {href} = el.attribs
+                if(!href) console.log("** ERR: deserialized <a> with no href")
                 return {
                     kind: 'block',
                     type: 'link',
-                    data: {href: el.attribs.href},
+                    data: {href: href},
                     nodes: next(el.children)
                 }
             }
             if(el.type == 'text') return
             if(BLOCK_TAGS[el.tagName] || MARK_TAGS[el.tagName]) return
-			console.log("No deserializer for: ", el.tagName, el)
+            console.log("No deserializer for: ", el.tagName, el)
         },
         serialize: (object, children) => {
             if(object.kind == 'string') return;
             if(object.kind == 'block' && object.type == 'link') {
-                console.log("Serialized <a>, the href is", object.data.get('href'), JSON.stringify(object.data, null, 2))
-                return <a href={object.data.get('href')}>{children}</a>
+                const href = object.data.get('href')
+                if(!href) console.log("** ERR: serializing <a> with no href", JSON.stringify(object.data, null, 2))
+                return <a href={href}>{children}</a>
             }
             if(object.kind == 'block' && object.type == 'image') {
                 const data = object.data
                 const src = data.get('src')
+                if(!src) {
+                  console.log("** ERR: serializing image with no src...")
+                  console.log("Serializing image.... data:",   JSON.stringify(data))
+                  console.log("Serializing image.... object:", JSON.stringify(object))
+                  console.log("Serializing image.... state:",  JSON.stringify(object.state))
+                  console.log("Serializing image.... node:",   JSON.stringify(object.node))
+                }
                 return <img src={src} />
             }
-			console.log("No serializer for: ", object.kind, JSON.stringify(object, null, 2), children)
+            console.log("No serializer for: ", object.kind, JSON.stringify(object, null, 2), children)
         }
     }
 ]
 
 export const schema = {
     defaultNode: 'paragraph',
+    toolbarMarks: [
+        { type: 'bold',      label: <strong>B</strong> },
+        { type: 'italic',    label: <i>I</i> },
+        { type: 'underline', label: <u>U</u> },
+        { type: 'strike',    label: <del>S</del> },
+        { type: 'code',      label: <code>{'{}'}</code> },
+        { type: 'sup',       label: <span>x<sup>2</sup></span> },
+        { type: 'sub',       label: <span>x<sub>2</sub></span> },
+    ],
+
 /*
     blockTypes: {
       ...Blocks,
     },
-    toolbarMarks: [
-        { type: 'bold',      icon: 'bold' },
-        { type: 'italic',    icon: 'italic' },
-        { type: 'underline', icon: 'underline' },
-        { type: 'code',      icon: 'code' },
-    ],
     toolbarTypes: [
         { type: 'heading-one',   icon: 'header' },
         { type: 'heading-two',   icon: 'header' },
@@ -175,6 +190,7 @@ export const schema = {
     ],
     sidebarTypes: [],
 */
+
     nodes: {
         'block':         ({ children }) => <p style={{background: 'red'}}>{children}</p>,
         'paragraph':     ({ children }) => <p>{children}</p>,
@@ -187,24 +203,22 @@ export const schema = {
         'heading-four':  ({ children }) => <h4>{children}</h4>,
         'bulleted-list-item': ({ children }) => <li>{children}</li>,
         'numbered-list-item': ({ children }) => <li>{children}</li>,
-        'image': (props) => {
-			const { data } = props.node
-			const src = data.get('src')
-			return <img {...props.attributes} src={'https://img1.steemit.com/0x0/' + src} />
-		},
-		'link':  (props) => {
-			const { data } = props.node
-			const href = data.get('href')
-console.log("rendering link...href=",href)
-			return <a {...props.attributes} href={href}>{props.children}</a>
-		},
+        'image': Image,
+        'link':  (props) => {
+            const { data } = props.node
+            const href = data.get('href')
+            return <a {...props.attributes} href={href}>{props.children}</a>
+        },
     },
+
     marks: {
         bold:      props => <strong>{props.children}</strong>,
         code:      props => <code>{props.children}</code>,
         italic:    props => <em>{props.children}</em>,
         underline: props => <u>{props.children}</u>,
         strike:    props => <del>{props.children}</del>,
+        sub:       props => <sub>{props.children}</sub>,
+        sup:       props => <sup>{props.children}</sup>,
     },
 }
 
diff --git a/package.json b/package.json
index 4217efa72..68eeca55d 100644
--- a/package.json
+++ b/package.json
@@ -114,6 +114,7 @@
     "sequelize": "^3.21.0",
     "sequelize-cli": "^2.3.1",
     "slate": "^0.14.13",
+    "slate-drop-or-paste-images": "^0.2.0",
     "speakingurl": "^9.0.0",
     "style-loader": "^0.13.0",
     "svg-inline-loader": "^0.4.0",
-- 
GitLab