From efddf7440465bcc7b0baf3f9ad6d89e36630f601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Mon, 8 Aug 2016 18:51:53 -0300 Subject: [PATCH] Image Uploads --- src/components/Widgets/MarkdownControl.css | 40 +-------- src/components/Widgets/MarkdownControl.js | 88 +++++++++++++++---- .../BlockTypesMenu.css | 4 + .../MarkdownControlElements/BlockTypesMenu.js | 43 ++++++++- .../localRenderers.css | 3 - .../MarkdownControlElements/localRenderers.js | 2 +- 6 files changed, 118 insertions(+), 62 deletions(-) delete mode 100644 src/components/Widgets/MarkdownControlElements/localRenderers.css diff --git a/src/components/Widgets/MarkdownControl.css b/src/components/Widgets/MarkdownControl.css index c87888af..352e12a3 100644 --- a/src/components/Widgets/MarkdownControl.css +++ b/src/components/Widgets/MarkdownControl.css @@ -1,39 +1,3 @@ - -.button { - color: #ccc; - cursor: pointer; -} - -.button[data-active="true"] { - color: black; -} - - -.menu > * { - display: inline-block; -} - -.menu > * + * { - margin-left: 10px; -} - -.hoverMenu { - padding: 8px 7px 6px; - position: absolute; - z-index: 1; - top: -10000px; - left: -10000px; - margin-top: -6px; - opacity: 0; - background-color: #222; - border-radius: 4px; - transition: opacity .75s; -} - -.hoverMenu .button { - color: #aaa; -} - -.hoverMenu .button[data-active="true"] { - color: #fff; +.active { + box-shadow: 0 0 0 2px blue; } diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js index 4f41f969..26c576c4 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl.js @@ -6,8 +6,7 @@ import Markdown from 'slate-markdown-serializer'; import { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers'; import StylesMenu from './MarkdownControlElements/StylesMenu'; import BlockTypesMenu from './MarkdownControlElements/BlockTypesMenu'; - -const markdown = new Markdown(); +import styles from './MarkdownControl.css'; /** * Slate Render Configuration @@ -15,6 +14,13 @@ const markdown = new Markdown(); class MarkdownControl extends React.Component { constructor(props) { super(props); + + this.customMarkdownSerialize = this.customMarkdownSerialize.bind(this); + this.markdown = new Markdown({ rules: [{ serialize: this.customMarkdownSerialize }] }); + + this.customImageNodeRenderer = this.customImageNodeRenderer.bind(this); + NODES['image'] = this.customImageNodeRenderer; + this.blockEdit = false; this.menuPositions = { stylesMenu: { @@ -32,7 +38,7 @@ class MarkdownControl extends React.Component { }; this.state = { - state: props.value ? markdown.deserialize(props.value) : Plain.deserialize('') + state: props.value ? this.markdown.deserialize(props.value) : Plain.deserialize('') }; this.handleChange = this.handleChange.bind(this); @@ -41,6 +47,8 @@ class MarkdownControl extends React.Component { this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this); this.handleInlineClick = this.handleInlineClick.bind(this); this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this); + this.handleImageClick = this.handleImageClick.bind(this); + this.focusAndAddParagraph = this.focusAndAddParagraph.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.calculateHoverMenuPosition = _.throttle(this.calculateHoverMenuPosition.bind(this), 100); this.calculateBlockMenuPosition = _.throttle(this.calculateBlockMenuPosition.bind(this), 100); @@ -49,6 +57,28 @@ class MarkdownControl extends React.Component { this.renderMark = this.renderMark.bind(this); } + + /** + * The two custom methods customMarkdownSerialize and customImageNodeRenderer make sure that + * both Markdown serializer and Node renderers have access to getMedia with the latest state. + */ + customMarkdownSerialize(obj, children) { + if (obj.kind === 'block' && obj.type === 'image') { + const src = this.props.getMedia(obj.getIn(['data', 'src'])); + const alt = obj.getIn(['data', 'alt']) || ''; + return `![${alt}](${src})`; + } + } + customImageNodeRenderer(editorProps) { + const { node, state } = editorProps; + const isFocused = state.selection.hasEdgeIn(node); + const className = isFocused ? styles.active : null; + const src = node.data.get('src'); + return ( + + ); + } + /** * Slate keeps track of selections, scroll position etc. * So, onChange gets dispatched on every interaction (click, arrows, everything...) @@ -65,7 +95,7 @@ class MarkdownControl extends React.Component { } handleDocumentChange(document, state) { - this.props.onChange(markdown.serialize(state)); + this.props.onChange(this.markdown.serialize(state)); } calculateHoverMenuPosition() { @@ -201,22 +231,41 @@ class MarkdownControl extends React.Component { }) .apply(); - this.setState({ state }, () => { - const blocks = this.state.state.document.getBlocks(); - const last = blocks.last(); - const normalized = state - .transform() - .focus() - .collapseToEndOf(last) - .splitBlock() - .setBlock(DEFAULT_NODE) - .apply({ - snapshot: false - }); - this.setState({ state:normalized }); - }); + this.setState({ state }, this.focusAndAddParagraph); } + handleImageClick(mediaProxy) { + let { state } = this.state; + this.props.onAddMedia(mediaProxy); + state = state + .transform() + .insertBlock({ + type: 'image', + isVoid: true, + data: { src: mediaProxy.path } + }) + .apply(); + + this.setState({ state }, this.focusAndAddParagraph); + } + + focusAndAddParagraph() { + const { state } = this.state; + const blocks = state.document.getBlocks(); + const last = blocks.last(); + const normalized = state + .transform() + .focus() + .collapseToEndOf(last) + .splitBlock() + .setBlock(DEFAULT_NODE) + .apply({ + snapshot: false + }); + this.setState({ state:normalized }); + } + + handleKeyDown(evt) { if (evt.shiftKey && evt.key === 'Enter') { this.blockEdit = true; @@ -249,6 +298,7 @@ class MarkdownControl extends React.Component { isOpen={isOpen} position={this.menuPositions.blockTypesMenu} onClickBlock={this.handleBlockTypeClick} + onClickImage={this.handleImageClick} /> ); } @@ -294,5 +344,7 @@ export default MarkdownControl; MarkdownControl.propTypes = { onChange: PropTypes.func.isRequired, + onAddMedia: PropTypes.func.isRequired, + getMedia: PropTypes.func.isRequired, value: PropTypes.node, }; diff --git a/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.css b/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.css index 5f5226cb..9868af79 100644 --- a/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.css +++ b/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.css @@ -26,3 +26,7 @@ cursor: pointer; color: #555; } + +.input { + display: none; +} diff --git a/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.js b/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.js index a040a3e2..a05a155e 100644 --- a/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.js +++ b/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.js @@ -1,6 +1,7 @@ import React, { Component, PropTypes } from 'react'; import Portal from 'react-portal'; import { Icon } from '../../UI'; +import MediaProxy from '../../../valueObjects/MediaProxy'; import styles from './BlockTypesMenu.css'; export default class BlockTypesMenu extends Component { @@ -16,6 +17,8 @@ export default class BlockTypesMenu extends Component { this.toggleMenu = this.toggleMenu.bind(this); this.handleOpen = this.handleOpen.bind(this); this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this); + this.handleFileUploadClick = this.handleFileUploadClick.bind(this); + this.handleFileUploadChange = this.handleFileUploadChange.bind(this); this.renderBlockTypeButton = this.renderBlockTypeButton.bind(this); } @@ -52,9 +55,36 @@ export default class BlockTypesMenu extends Component { } handleBlockTypeClick(e, type) { - this.props.onClickBlock(type, false, false); + this.props.onClickBlock(type); } + handleFileUploadClick() { + this._fileInput.click(); + } + + handleFileUploadChange(e) { + e.stopPropagation(); + e.preventDefault(); + + const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files; + const files = [...fileList]; + const imageType = /^image\//; + + // Iterate through the list of files and return the first image on the list + const file = files.find((currentFile) => { + if (imageType.test(currentFile.type)) { + return currentFile; + } + }); + + if (file) { + const mediaProxy = new MediaProxy(file.name, file); + this.props.onClickImage(mediaProxy); + } + + } + + renderBlockTypeButton(type, icon) { const onClick = e => this.handleBlockTypeClick(e, type); return ( @@ -67,6 +97,14 @@ export default class BlockTypesMenu extends Component { return (
{this.renderBlockTypeButton('horizontal-rule', 'dot-3')} + + this._fileInput = el} + />
); } else { @@ -100,5 +138,6 @@ BlockTypesMenu.propTypes = { top: PropTypes.number.isRequired, left: PropTypes.number.isRequired }), - onClickBlock: PropTypes.func.isRequired + onClickBlock: PropTypes.func.isRequired, + onClickImage: PropTypes.func.isRequired }; diff --git a/src/components/Widgets/MarkdownControlElements/localRenderers.css b/src/components/Widgets/MarkdownControlElements/localRenderers.css deleted file mode 100644 index 4ceb194b..00000000 --- a/src/components/Widgets/MarkdownControlElements/localRenderers.css +++ /dev/null @@ -1,3 +0,0 @@ -.active { - box-shadow: 0 0 0 2px blue; -} diff --git a/src/components/Widgets/MarkdownControlElements/localRenderers.js b/src/components/Widgets/MarkdownControlElements/localRenderers.js index d6a133af..951d1685 100644 --- a/src/components/Widgets/MarkdownControlElements/localRenderers.js +++ b/src/components/Widgets/MarkdownControlElements/localRenderers.js @@ -1,6 +1,6 @@ import React from 'react'; import Block from './Block'; -import styles from './localRenderers.css' +import styles from '../MarkdownControl.css'; /* eslint react/prop-types: 0, react/no-multi-comp: 0 */