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