added dropzone that can handle previews

This commit is contained in:
bill bittner 2018-01-10 16:30:17 -08:00
parent ad0e43a428
commit 70c75976fb
7 changed files with 251 additions and 60 deletions

View file

@ -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 */

View file

@ -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;
}

View file

@ -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,19 +116,21 @@ 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}>
{ this.state.dragOver ? ( <div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
<div id="dropbzone-dragover"> { this.state.dragOver ? (
<p className="blue">Drop it.</p> <div id="dropzone-dragover">
</div> <p className="blue">Drop it.</p>
) : ( </div>
<div id="primary-dropzone-instructions"> ) : (
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.state.fileError}</p> <div id="dropzone-instructions">
<p>Drag & drop image or video here to publish</p> <p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.state.fileError}</p>
<p className="fine-print">OR</p> <p>Drag & drop image or video here to publish</p>
<p className="blue--underlined">CHOOSE FILE</p> <p className="fine-print">OR</p>
</div> <p className="blue--underlined">CHOOSE FILE</p>
)} </div>
)}
</div>
</div> </div>
</div> </div>
); );
@ -148,4 +151,4 @@ const mapDispatchToProps = dispatch => {
}; };
} }
export default connect(mapStateToProps, mapDispatchToProps)(PublishDropzone); export default connect(mapStateToProps, mapDispatchToProps)(Dropzone);

View 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);

View file

@ -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);
}
}
} }
previewFile (file) { handleDragOver (event) {
console.log('previewFile', file) event.preventDefault();
const that = this; }
if (file.type !== 'video/mp4') { handleDragEnd (event) {
const previewReader = new FileReader(); var dt = event.dataTransfer;
previewReader.readAsDataURL(file); if (dt.items) {
previewReader.onloadend = function () { for (var i = 0; i < dt.items.length; i++) {
that.setState({previewSource: previewReader.result}); dt.items.remove(i);
}; }
} 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"/>
<p className="fine-print">OR</p> </form>
<p className="blue--underlined">CHOOSE FILE</p> <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="blue--underlined">CHOOSE FILE</p>
</div>
) : (
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>
<img id="asset-preview" src={this.state.previewSource} alt="publish preview"/>
</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);

View file

@ -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
View 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.');
}
},
}