Image Uploads
This commit is contained in:
parent
3b1590be72
commit
efddf74404
@ -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;
|
||||
}
|
||||
|
@ -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 ``;
|
||||
}
|
||||
}
|
||||
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.
|
||||
* 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,
|
||||
};
|
||||
|
@ -26,3 +26,7 @@
|
||||
cursor: pointer;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: none;
|
||||
}
|
||||
|
@ -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 (
|
||||
<div className={styles.menu}>
|
||||
{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>
|
||||
);
|
||||
} 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
|
||||
};
|
||||
|
@ -1,3 +0,0 @@
|
||||
.active {
|
||||
box-shadow: 0 0 0 2px blue;
|
||||
}
|
@ -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 */
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user