Support for drag and drop image uploads in both rte and raw editor mode

This commit is contained in:
Mathias Biilmann Christensen 2016-12-27 23:18:37 -08:00
parent 2ed2160c92
commit 75100eaa3a
5 changed files with 138 additions and 14 deletions

View File

@ -1,3 +1,22 @@
.root { .root {
position: relative; position: relative;
} }
.dragging { }
.shim {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
border: 2px dashed #aaa;
background: rgba(0,0,0,0.2);
}
.dragging .shim {
z-index: 1000;
display: block;
pointer-events: none;
}

View File

@ -24,10 +24,6 @@ function processUrl(url) {
return `/${ url }`; return `/${ url }`;
} }
function preventDefault(e) {
e.preventDefault();
}
function cleanupPaste(paste) { function cleanupPaste(paste) {
const content = html.toContent(paste); const content = html.toContent(paste);
return markdown.toText(content); return markdown.toText(content);
@ -76,11 +72,9 @@ export default class RawEditor extends React.Component {
}, },
}; };
} }
componentDidMount() { componentDidMount() {
this.updateHeight(); this.updateHeight();
this.element.addEventListener('dragenter', preventDefault, false);
this.element.addEventListener('dragover', preventDefault, false);
this.element.addEventListener('drop', this.handleDrop, false);
this.element.addEventListener('paste', this.handlePaste, false); this.element.addEventListener('paste', this.handlePaste, false);
} }
@ -93,9 +87,7 @@ export default class RawEditor extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
this.element.removeEventListener('dragenter', preventDefault); this.element.removeEventListener('paste', this.handlePaste);
this.element.removeEventListener('dragover', preventDefault);
this.element.removeEventListener('drop', this.handleDrop);
} }
getSelection() { getSelection() {
@ -256,8 +248,25 @@ export default class RawEditor extends React.Component {
}; };
} }
handleDragEnter = (e) => {
e.preventDefault();
this.setState({ dragging: true });
};
handleDragLeave = (e) => {
e.preventDefault();
this.setState({ dragging: false });
};
handleDragOver = (e) => {
e.preventDefault();
};
handleDrop = (e) => { handleDrop = (e) => {
e.preventDefault(); e.preventDefault();
this.setState({ dragging: false });
let data; let data;
if (e.dataTransfer.files && e.dataTransfer.files.length) { if (e.dataTransfer.files && e.dataTransfer.files.length) {
@ -296,8 +305,19 @@ export default class RawEditor extends React.Component {
render() { render() {
const { onAddMedia, onRemoveMedia, getMedia } = this.props; const { onAddMedia, onRemoveMedia, getMedia } = this.props;
const { showToolbar, showBlockMenu, plugins, selectionPosition } = this.state; const { showToolbar, showBlockMenu, plugins, selectionPosition, dragging } = this.state;
return (<div className={styles.root}> const classNames = [styles.root];
if (dragging) {
classNames.push(styles.dragging);
}
return (<div
className={classNames.join(' ')}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
>
<Toolbar <Toolbar
isOpen={showToolbar} isOpen={showToolbar}
selectionPosition={selectionPosition} selectionPosition={selectionPosition}
@ -324,6 +344,7 @@ export default class RawEditor extends React.Component {
onChange={this.handleChange} onChange={this.handleChange}
onSelect={this.handleSelection} onSelect={this.handleSelection}
/> />
<div className={styles.shim}/>
</div>); </div>);
} }
} }

View File

@ -20,6 +20,13 @@
& p { & p {
margin-bottom: 20px; margin-bottom: 20px;
} }
& hr {
border: 1px solid;
margin-bottom: 20px;
}
& li > p {
margin: 0;
}
& div[data-plugin] { & div[data-plugin] {
background: #fff; background: #fff;
border: 1px solid #aaa; border: 1px solid #aaa;
@ -28,6 +35,25 @@
} }
} }
.dragging { }
.shim {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
border: 2px dashed #aaa;
background: rgba(0,0,0,0.2);
}
.dragging .shim {
z-index: 1000;
display: block;
pointer-events: none;
}
:global { :global {
& .ProseMirror { & .ProseMirror {
position: relative; position: relative;

View File

@ -11,6 +11,7 @@ import { keymap } from 'prosemirror-keymap';
import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown'; import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown';
import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands'; import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
import registry from '../../../../lib/registry'; import registry from '../../../../lib/registry';
import MediaProxy from '../../../../valueObjects/MediaProxy';
import { buildKeymap } from './keymap'; import { buildKeymap } from './keymap';
import createMarkdownParser from './parser'; import createMarkdownParser from './parser';
import Toolbar from '../Toolbar'; import Toolbar from '../Toolbar';
@ -213,15 +214,71 @@ export default class Editor extends Component {
this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action()); this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action());
}; };
handleDragEnter = (e) => {
e.preventDefault();
this.setState({ dragging: true });
};
handleDragLeave = (e) => {
e.preventDefault();
this.setState({ dragging: false });
};
handleDragOver = (e) => {
e.preventDefault();
};
handleDrop = (e) => {
e.preventDefault();
this.setState({ dragging: false });
const { schema } = this.state;
const nodes = [];
if (e.dataTransfer.files && e.dataTransfer.files.length) {
Array.from(e.dataTransfer.files).forEach((file) => {
const mediaProxy = new MediaProxy(file.name, file);
this.props.onAddMedia(mediaProxy);
if (file.type.split('/')[0] === 'image') {
nodes.push(
schema.nodes.image.create({ src: mediaProxy.public_path, alt: file.name })
);
} else {
nodes.push(
schema.marks.link.create({ href: mediaProxy.public_path, title: file.name })
);
}
});
} else {
nodes.push(schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain')));
}
nodes.forEach((node) => {
this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action());
});
};
handleToggle = () => { handleToggle = () => {
this.props.onMode('raw'); this.props.onMode('raw');
}; };
render() { render() {
const { onAddMedia, onRemoveMedia, getMedia } = this.props; const { onAddMedia, onRemoveMedia, getMedia } = this.props;
const { plugins, showToolbar, showBlockMenu, selectionPosition } = this.state; const { plugins, showToolbar, showBlockMenu, selectionPosition, dragging } = this.state;
const classNames = [styles.editor];
if (dragging) {
classNames.push(styles.dragging);
}
return (<div className={styles.editor}> return (<div
className={classNames.join(' ')}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
>
<Toolbar <Toolbar
isOpen={showToolbar} isOpen={showToolbar}
selectionPosition={selectionPosition} selectionPosition={selectionPosition}
@ -242,6 +299,7 @@ export default class Editor extends Component {
getMedia={getMedia} getMedia={getMedia}
/> />
<div ref={this.handleRef} /> <div ref={this.handleRef} />
<div className={styles.shim} />
</div>); </div>);
} }
} }