added dropzone that can handle previews
This commit is contained in:
parent
ad0e43a428
commit
70c75976fb
7 changed files with 251 additions and 60 deletions
|
@ -483,26 +483,25 @@ table {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#primary-dropzone-instructions, #dropbzone-dragover {
|
#dropzone-text-holder {
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-absolute {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
left: 0;
|
||||||
left: 0px;
|
top: 0;
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#asset-preview-holder {
|
#dropzone-dragover, #dropzone-instructions {
|
||||||
position: relative;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#asset-preview {
|
#asset-preview {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.5rem;
|
width: 100%;
|
||||||
width: calc(100% - 1rem);
|
}
|
||||||
|
|
||||||
|
.dim {
|
||||||
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Assets */
|
/* Assets */
|
||||||
|
|
|
@ -1,10 +1,2 @@
|
||||||
function preview_onmouseenter_handler () {
|
|
||||||
document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'flex-container--column flex-container--center-center position-absolute');
|
|
||||||
document.getElementById('asset-preview').style.opacity = 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function preview_onmouseleave_handler () {
|
|
||||||
document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'hidden');
|
|
||||||
document.getElementById('asset-preview').style.opacity = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,13 @@ import React from 'react';
|
||||||
import { selectFile } from '../actions';
|
import { selectFile } from '../actions';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
class PublishDropzone extends React.Component {
|
class Dropzone extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
fileError: null,
|
fileError: null,
|
||||||
dragOver : false,
|
dragOver : false,
|
||||||
|
|
||||||
}
|
}
|
||||||
this.handleDrop = this.handleDrop.bind(this);
|
this.handleDrop = this.handleDrop.bind(this);
|
||||||
this.handleDragOver = this.handleDragOver.bind(this);
|
this.handleDragOver = this.handleDragOver.bind(this);
|
||||||
|
@ -115,13 +116,14 @@ class PublishDropzone extends React.Component {
|
||||||
<form>
|
<form>
|
||||||
<input className="input-file" type="file" id="file_input" name="file_input" accept="video/*,image/*" onChange={this.handleFileInput} encType="multipart/form-data"/>
|
<input className="input-file" type="file" id="file_input" name="file_input" accept="video/*,image/*" onChange={this.handleFileInput} encType="multipart/form-data"/>
|
||||||
</form>
|
</form>
|
||||||
<div id="primary-dropzone" className={'dropzone row row--padded row--tall flex-container--column flex-container--center-center' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onClick={this.handleClick}>
|
<div id="primary-dropzone" className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onClick={this.handleClick}>
|
||||||
|
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
|
||||||
{ this.state.dragOver ? (
|
{ this.state.dragOver ? (
|
||||||
<div id="dropbzone-dragover">
|
<div id="dropzone-dragover">
|
||||||
<p className="blue">Drop it.</p>
|
<p className="blue">Drop it.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div id="primary-dropzone-instructions">
|
<div id="dropzone-instructions">
|
||||||
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.state.fileError}</p>
|
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.state.fileError}</p>
|
||||||
<p>Drag & drop image or video here to publish</p>
|
<p>Drag & drop image or video here to publish</p>
|
||||||
<p className="fine-print">OR</p>
|
<p className="fine-print">OR</p>
|
||||||
|
@ -130,6 +132,7 @@ class PublishDropzone extends React.Component {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -148,4 +151,4 @@ const mapDispatchToProps = dispatch => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(PublishDropzone);
|
export default connect(mapStateToProps, mapDispatchToProps)(Dropzone);
|
53
react/components/Preview.jsx
Normal file
53
react/components/Preview.jsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
|
class Preview extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
previewSource: '',
|
||||||
|
}
|
||||||
|
this.previewFile = this.previewFile.bind(this);
|
||||||
|
}
|
||||||
|
componentWillMount () {
|
||||||
|
console.log('Preview will mount');
|
||||||
|
if (this.props.file) {
|
||||||
|
this.previewFile(this.props.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentWillReceiveProps ({ file }) {
|
||||||
|
console.log('Preview will receive props');
|
||||||
|
this.previewFile(file);
|
||||||
|
}
|
||||||
|
previewFile (file) {
|
||||||
|
console.log('previewFile', file)
|
||||||
|
const that = this;
|
||||||
|
if (file.type !== 'video/mp4') {
|
||||||
|
const previewReader = new FileReader();
|
||||||
|
previewReader.readAsDataURL(file);
|
||||||
|
previewReader.onloadend = function () {
|
||||||
|
that.setState({previewSource: previewReader.result});
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
that.setState({previewSource: '/assets/img/video_thumb_default.png'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
id="asset-preview"
|
||||||
|
src={this.state.previewSource}
|
||||||
|
className={this.props.dimPreview ? 'dim' : ''}
|
||||||
|
alt="publish preview"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
file: state.file,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(Preview);
|
|
@ -1,40 +1,138 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
// import PropTypes from 'prop-types';
|
||||||
|
import { selectFile } from '../actions';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Preview from './Preview.jsx';
|
||||||
|
|
||||||
|
import { validateFile } from '../utils/file.js';
|
||||||
|
|
||||||
class PreviewDropzone extends React.Component {
|
class PreviewDropzone extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
previewSource: '',
|
fileError : null,
|
||||||
|
dragOver : false,
|
||||||
|
mouseOver : false,
|
||||||
|
dimPreview: false,
|
||||||
}
|
}
|
||||||
this.previewFile = this.previewFile.bind(this);
|
this.handleDrop = this.handleDrop.bind(this);
|
||||||
|
this.handleDragOver = this.handleDragOver.bind(this);
|
||||||
|
this.handleDragEnd = this.handleDragEnd.bind(this);
|
||||||
|
this.handleDragEnter = this.handleDragEnter.bind(this);
|
||||||
|
this.handleDragLeave = this.handleDragLeave.bind(this);
|
||||||
|
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||||
|
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
this.handleFileInput = this.handleFileInput.bind(this);
|
||||||
|
this.selectFile = this.selectFile.bind(this);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
handleDrop (event) {
|
||||||
console.log('props after mount', this.props);
|
event.preventDefault();
|
||||||
this.previewFile(this.props.file);
|
this.setState({dragOver: false});
|
||||||
|
// if dropped items aren't files, reject them
|
||||||
|
const dt = event.dataTransfer;
|
||||||
|
console.log('dt', dt);
|
||||||
|
if (dt.items) {
|
||||||
|
if (dt.items[0].kind == 'file') {
|
||||||
|
const droppedFile = dt.items[0].getAsFile();
|
||||||
|
this.selectFile(droppedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleDragOver (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
handleDragEnd (event) {
|
||||||
|
var dt = event.dataTransfer;
|
||||||
|
if (dt.items) {
|
||||||
|
for (var i = 0; i < dt.items.length; i++) {
|
||||||
|
dt.items.remove(i);
|
||||||
}
|
}
|
||||||
previewFile (file) {
|
|
||||||
console.log('previewFile', file)
|
|
||||||
const that = this;
|
|
||||||
if (file.type !== 'video/mp4') {
|
|
||||||
const previewReader = new FileReader();
|
|
||||||
previewReader.readAsDataURL(file);
|
|
||||||
previewReader.onloadend = function () {
|
|
||||||
that.setState({previewSource: previewReader.result});
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
that.setState({previewSource: '/assets/img/video_thumb_default.png'});
|
event.dataTransfer.clearData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleDragEnter () {
|
||||||
|
this.setState({dragOver: true, dimPreview: true});
|
||||||
|
}
|
||||||
|
handleDragLeave () {
|
||||||
|
this.setState({dragOver: false, dimPreview: false});
|
||||||
|
}
|
||||||
|
handleMouseEnter () {
|
||||||
|
this.setState({mouseOver: true, dimPreview: true});
|
||||||
|
}
|
||||||
|
handleMouseLeave () {
|
||||||
|
this.setState({mouseOver: false, dimPreview: false});
|
||||||
|
}
|
||||||
|
handleClick (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
// trigger file input
|
||||||
|
document.getElementById('file_input').click();
|
||||||
|
}
|
||||||
|
handleFileInput (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const fileList = event.target.files;
|
||||||
|
this.selectFile(fileList[0]);
|
||||||
|
}
|
||||||
|
selectFile (file) {
|
||||||
|
if (file) {
|
||||||
|
try {
|
||||||
|
validateFile(file); // validate the file's name, type, and size
|
||||||
|
} catch (error) {
|
||||||
|
return this.setState({fileError: error.message});
|
||||||
|
}
|
||||||
|
// stage it so it will be ready when the publish button is clicked
|
||||||
|
this.setState({fileError: null});
|
||||||
|
this.props.onFileSelect(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div id="asset-preview-holder" className="dropzone">
|
<div className="row row--tall flex-container--column">
|
||||||
<div id="asset-preview-dropzone-instructions" className="hidden">
|
<form>
|
||||||
<p>Drag & drop image or video here</p>
|
<input className="input-file" type="file" id="file_input" name="file_input" accept="video/*,image/*" onChange={this.handleFileInput} encType="multipart/form-data"/>
|
||||||
|
</form>
|
||||||
|
<div id="preview-dropzone" className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick}>
|
||||||
|
{this.props.file ? (
|
||||||
|
<div>
|
||||||
|
<Preview dimPreview={this.state.dimPreview}/>
|
||||||
|
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
|
||||||
|
{ this.state.dragOver ? (
|
||||||
|
<div id="dropzone-dragover">
|
||||||
|
<p className="blue">Drop it.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
null
|
||||||
|
)}
|
||||||
|
{ this.state.mouseOver ? (
|
||||||
|
<div id="dropzone-instructions">
|
||||||
|
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.state.fileError}</p>
|
||||||
|
<p>Drag & drop image or video here to publish</p>
|
||||||
<p className="fine-print">OR</p>
|
<p className="fine-print">OR</p>
|
||||||
<p className="blue--underlined">CHOOSE FILE</p>
|
<p className="blue--underlined">CHOOSE FILE</p>
|
||||||
</div>
|
</div>
|
||||||
<img id="asset-preview" src={this.state.previewSource} alt="publish preview"/>
|
) : (
|
||||||
|
null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
|
||||||
|
{ this.state.dragOver ? (
|
||||||
|
<div id="dropzone-dragover">
|
||||||
|
<p className="blue">Drop it.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div id="dropzone-instructions">
|
||||||
|
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.state.fileError}</p>
|
||||||
|
<p>Drag & drop image or video here to publish</p>
|
||||||
|
<p className="fine-print">OR</p>
|
||||||
|
<p className="blue--underlined">CHOOSE FILE</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -46,4 +144,12 @@ const mapStateToProps = state => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(PreviewDropzone);
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
onFileSelect: (file) => {
|
||||||
|
dispatch(selectFile(file));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PreviewDropzone);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PublishDropzone from './PublishDropzone.jsx';
|
import PreviewDropzone from './PreviewDropzone.jsx';
|
||||||
import PublishForm from './PublishForm.jsx';
|
import PublishForm from './PublishForm.jsx';
|
||||||
import PublishStatus from './PublishStatus.jsx';
|
import PublishStatus from './PublishStatus.jsx';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
@ -11,7 +11,7 @@ class PublishTool extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className="row row--tall flex-container--column">
|
<div className="row row--tall flex-container--column">
|
||||||
{ !this.props.file && <PublishDropzone /> }
|
{ !this.props.file && <PreviewDropzone /> }
|
||||||
{ this.props.file && <PublishForm /> }
|
{ this.props.file && <PublishForm /> }
|
||||||
{ this.props.publishStatus && <PublishStatus /> }
|
{ this.props.publishStatus && <PublishStatus /> }
|
||||||
</div>
|
</div>
|
||||||
|
|
38
react/utils/file.js
Normal file
38
react/utils/file.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
module.exports = {
|
||||||
|
validateFile (file) {
|
||||||
|
if (!file) {
|
||||||
|
console.log('no file found');
|
||||||
|
throw new Error('no file provided');
|
||||||
|
}
|
||||||
|
if (/'/.test(file.name)) {
|
||||||
|
console.log('file name had apostrophe in it');
|
||||||
|
throw new Error('apostrophes are not allowed in the file name');
|
||||||
|
}
|
||||||
|
// validate size and type
|
||||||
|
switch (file.type) {
|
||||||
|
case 'image/jpeg':
|
||||||
|
case 'image/jpg':
|
||||||
|
case 'image/png':
|
||||||
|
if (file.size > 10000000) {
|
||||||
|
console.log('file was too big');
|
||||||
|
throw new Error('Sorry, images are limited to 10 megabytes.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'image/gif':
|
||||||
|
if (file.size > 50000000) {
|
||||||
|
console.log('file was too big');
|
||||||
|
throw new Error('Sorry, .gifs are limited to 50 megabytes.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'video/mp4':
|
||||||
|
if (file.size > 50000000) {
|
||||||
|
console.log('file was too big');
|
||||||
|
throw new Error('Sorry, videos are limited to 50 megabytes.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('file type is not supported');
|
||||||
|
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
Loading…
Reference in a new issue