Skip to content

Commit

Permalink
Feat: custom upload control for images (elastic#507)
Browse files Browse the repository at this point in the history
Add a custom file upload component, which is used in the image uploader to provide a nicer, styled upload control than the old file input

* chore: make image upload a normal component

* fix: only update upload image state if still mounted

* feat: create a file upload component

useful for wrapping custom upload controls

* feat: use FileUpload for image upload argtype

* feat: handle drag and drop files

this mimics native file input functionality. also sends an activeTarget value into the render child so it can react to things being dragged over it

* feat: make drop target more apparent

use the activeTarget value to set the button class and text

* fix: make exposed prop clearer

also clean up and add some comments
  • Loading branch information
w33ble authored Apr 25, 2018
1 parent 6da85af commit 7f53828
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 39 deletions.
79 changes: 79 additions & 0 deletions public/components/file_upload/file_upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';

export class FileUpload extends React.PureComponent {
static propTypes = {
id: PropTypes.string,
className: PropTypes.string,
onUpload: PropTypes.func.isRequired,
children: PropTypes.func.isRequired,
};

static defaultProps = {
id: '',
className: 'canvas__file-upload',
};

state = {
isHovered: false,
};

// track enter count since enter/leave fires with each child node you drag over
_enterCount = 0;

uploadPrompt = ev => {
ev.preventDefault();
this.fileInput.click();
};

onDrag = type => ev => {
ev.preventDefault();
this._enterCount = type === 'enter' ? this._enterCount + 1 : this._enterCount - 1;
this.setState({ isHovered: this._enterCount > 0 });
};

onDragOver = ev => {
// enables the onDrop handler, see https://developer.mozilla.org/en-US/docs/Web/Events/drop#Example
ev.preventDefault();
};

onDrop = ev => {
ev.preventDefault();
this._enterCount = 0;
this.setState({ isHovered: false });

this.props.onUpload({
files: ev.dataTransfer.files,
});
};

onUpload = ev => {
ev.preventDefault();
this.props.onUpload({
files: ev.target.files,
});
};

render() {
const { id, className, children } = this.props;

return (
<div
id={id}
className={className}
onDragEnter={this.onDrag('enter')}
onDragLeave={this.onDrag('leave')}
onDragOver={this.onDragOver}
onDrop={this.onDrop}
>
<input
type="file"
style={{ display: 'none' }}
onChange={this.onUpload}
ref={r => (this.fileInput = r)}
/>
{children({ uploadPrompt: this.uploadPrompt, isHovered: this.state.isHovered })}
</div>
);
}
}
1 change: 1 addition & 0 deletions public/components/file_upload/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FileUpload } from './file_upload';
92 changes: 53 additions & 39 deletions public/expression_types/arg_types/image_upload.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormGroup, ControlLabel } from 'react-bootstrap';
import { withState } from 'recompose';
import { FormGroup, Button } from 'react-bootstrap';
import { FileUpload } from '../../components/file_upload';
import { encode } from '../../../common/lib/dataurl';
import { templateFromReactComponent } from '../../lib/template_from_react_component';

const ImageUploadArgInput = ({
typeInstance,
onAssetAdd,
onValueChange,
setLoading,
isLoading,
}) => {
const { name } = typeInstance;
class ImageUpload extends React.Component {
static propTypes = {
onAssetAdd: PropTypes.func.isRequired,
onValueChange: PropTypes.func.isRequired,
typeInstance: PropTypes.object.isRequired,
};

function handleUpload(ev) {
const [upload] = ev.target.files;
setLoading(true); // start loading indicator
state = {
loading: false,
};

componentWillUnmount() {
this._isMounted = false;
}

_isMounted = true;

handleUpload = ({ files }) => {
const { onAssetAdd, onValueChange } = this.props;
const [upload] = files;
this.setState({ loading: true }); // start loading indicator

return encode(upload)
.then(dataurl => onAssetAdd('dataurl', dataurl))
.then(assetId => {
setLoading(false); // stop loading indicator

onValueChange({
type: 'expression',
chain: [
Expand All @@ -35,36 +42,43 @@ const ImageUploadArgInput = ({
},
],
});
});
}

return (
<FormGroup key={name} controlId="formControlsSelect">
{isLoading && (
<ControlLabel>
Image uploading <span className="fa fa-spinner fa-pulse" />
</ControlLabel>
)}
<div className="canvas__argtype--image">
<input type="file" onChange={handleUpload} disabled={isLoading} />
</div>
</FormGroup>
);
};
// this component can go away when onValueChange is called, check for _isMounted
this._isMounted && this.setState({ loading: false }); // set loading state back to false
});
};

ImageUploadArgInput.propTypes = {
onAssetAdd: PropTypes.func.isRequired,
onValueChange: PropTypes.func.isRequired,
typeInstance: PropTypes.object.isRequired,
setLoading: PropTypes.func.isRequired,
isLoading: PropTypes.bool,
};
render() {
const { typeInstance } = this.props;
const isLoading = this.state.loading;

const EnhancedImageUpload = withState('isLoading', 'setLoading', false)(ImageUploadArgInput);
return (
<FormGroup key={typeInstance.name} controlId="formControlsSelect">
{isLoading ? (
<span>
Image uploading <span className="fa fa-spinner fa-pulse" />
</span>
) : (
<FileUpload onUpload={this.handleUpload}>
{({ uploadPrompt, isHovered }) => (
<Button
bsStyle={isHovered ? 'info' : 'default'}
bsSize="small"
onClick={uploadPrompt}
>
{isHovered ? 'Drop to Upload Image' : 'Upload New Image'}
</Button>
)}
</FileUpload>
)}
</FormGroup>
);
}
}

export const imageUpload = () => ({
name: 'imageUpload',
displayName: 'Image Upload',
help: 'Select or upload an image',
template: templateFromReactComponent(EnhancedImageUpload),
template: templateFromReactComponent(ImageUpload),
});

0 comments on commit 7f53828

Please sign in to comment.