Image Uploads

This commit is contained in:
Cássio Zen 2016-08-08 18:51:53 -03:00
parent 3b1590be72
commit efddf74404
6 changed files with 118 additions and 62 deletions

View File

@ -1,39 +1,3 @@
.active {
.button { box-shadow: 0 0 0 2px blue;
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;
} }

View File

@ -6,8 +6,7 @@ import Markdown from 'slate-markdown-serializer';
import { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers'; import { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers';
import StylesMenu from './MarkdownControlElements/StylesMenu'; import StylesMenu from './MarkdownControlElements/StylesMenu';
import BlockTypesMenu from './MarkdownControlElements/BlockTypesMenu'; import BlockTypesMenu from './MarkdownControlElements/BlockTypesMenu';
import styles from './MarkdownControl.css';
const markdown = new Markdown();
/** /**
* Slate Render Configuration * Slate Render Configuration
@ -15,6 +14,13 @@ const markdown = new Markdown();
class MarkdownControl extends React.Component { class MarkdownControl extends React.Component {
constructor(props) { constructor(props) {
super(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.blockEdit = false;
this.menuPositions = { this.menuPositions = {
stylesMenu: { stylesMenu: {
@ -32,7 +38,7 @@ class MarkdownControl extends React.Component {
}; };
this.state = { 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); this.handleChange = this.handleChange.bind(this);
@ -41,6 +47,8 @@ class MarkdownControl extends React.Component {
this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this); this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this);
this.handleInlineClick = this.handleInlineClick.bind(this); this.handleInlineClick = this.handleInlineClick.bind(this);
this.handleBlockTypeClick = this.handleBlockTypeClick.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.handleKeyDown = this.handleKeyDown.bind(this);
this.calculateHoverMenuPosition = _.throttle(this.calculateHoverMenuPosition.bind(this), 100); this.calculateHoverMenuPosition = _.throttle(this.calculateHoverMenuPosition.bind(this), 100);
this.calculateBlockMenuPosition = _.throttle(this.calculateBlockMenuPosition.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); 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 (
<img {...editorProps.attributes} src={this.props.getMedia(src)} className={className} />
);
}
/** /**
* Slate keeps track of selections, scroll position etc. * Slate keeps track of selections, scroll position etc.
* So, onChange gets dispatched on every interaction (click, arrows, everything...) * So, onChange gets dispatched on every interaction (click, arrows, everything...)
@ -65,7 +95,7 @@ class MarkdownControl extends React.Component {
} }
handleDocumentChange(document, state) { handleDocumentChange(document, state) {
this.props.onChange(markdown.serialize(state)); this.props.onChange(this.markdown.serialize(state));
} }
calculateHoverMenuPosition() { calculateHoverMenuPosition() {
@ -201,22 +231,41 @@ class MarkdownControl extends React.Component {
}) })
.apply(); .apply();
this.setState({ state }, () => { this.setState({ state }, this.focusAndAddParagraph);
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 });
});
} }
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) { handleKeyDown(evt) {
if (evt.shiftKey && evt.key === 'Enter') { if (evt.shiftKey && evt.key === 'Enter') {
this.blockEdit = true; this.blockEdit = true;
@ -249,6 +298,7 @@ class MarkdownControl extends React.Component {
isOpen={isOpen} isOpen={isOpen}
position={this.menuPositions.blockTypesMenu} position={this.menuPositions.blockTypesMenu}
onClickBlock={this.handleBlockTypeClick} onClickBlock={this.handleBlockTypeClick}
onClickImage={this.handleImageClick}
/> />
); );
} }
@ -294,5 +344,7 @@ export default MarkdownControl;
MarkdownControl.propTypes = { MarkdownControl.propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.node,
}; };

View File

@ -26,3 +26,7 @@
cursor: pointer; cursor: pointer;
color: #555; color: #555;
} }
.input {
display: none;
}

View File

@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import Portal from 'react-portal'; import Portal from 'react-portal';
import { Icon } from '../../UI'; import { Icon } from '../../UI';
import MediaProxy from '../../../valueObjects/MediaProxy';
import styles from './BlockTypesMenu.css'; import styles from './BlockTypesMenu.css';
export default class BlockTypesMenu extends Component { export default class BlockTypesMenu extends Component {
@ -16,6 +17,8 @@ export default class BlockTypesMenu extends Component {
this.toggleMenu = this.toggleMenu.bind(this); this.toggleMenu = this.toggleMenu.bind(this);
this.handleOpen = this.handleOpen.bind(this); this.handleOpen = this.handleOpen.bind(this);
this.handleBlockTypeClick = this.handleBlockTypeClick.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); this.renderBlockTypeButton = this.renderBlockTypeButton.bind(this);
} }
@ -52,9 +55,36 @@ export default class BlockTypesMenu extends Component {
} }
handleBlockTypeClick(e, type) { 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) { renderBlockTypeButton(type, icon) {
const onClick = e => this.handleBlockTypeClick(e, type); const onClick = e => this.handleBlockTypeClick(e, type);
return ( return (
@ -67,6 +97,14 @@ export default class BlockTypesMenu extends Component {
return ( return (
<div className={styles.menu}> <div className={styles.menu}>
{this.renderBlockTypeButton('horizontal-rule', 'dot-3')} {this.renderBlockTypeButton('horizontal-rule', 'dot-3')}
<Icon type="picture" onClick={this.handleFileUploadClick} className={styles.icon} />
<input
type="file"
accept="image/*"
onChange={this.handleFileUploadChange}
className={styles.input}
ref={(el) => this._fileInput = el}
/>
</div> </div>
); );
} else { } else {
@ -100,5 +138,6 @@ BlockTypesMenu.propTypes = {
top: PropTypes.number.isRequired, top: PropTypes.number.isRequired,
left: PropTypes.number.isRequired left: PropTypes.number.isRequired
}), }),
onClickBlock: PropTypes.func.isRequired onClickBlock: PropTypes.func.isRequired,
onClickImage: PropTypes.func.isRequired
}; };

View File

@ -1,3 +0,0 @@
.active {
box-shadow: 0 0 0 2px blue;
}

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import Block from './Block'; import Block from './Block';
import styles from './localRenderers.css' import styles from '../MarkdownControl.css';
/* eslint react/prop-types: 0, react/no-multi-comp: 0 */ /* eslint react/prop-types: 0, react/no-multi-comp: 0 */