Skip to content

Commit

Permalink
Merge pull request #241 from open-sausages/pulls/4.0/upload-preview
Browse files Browse the repository at this point in the history
Uploading preview adjustments
  • Loading branch information
chillu authored Sep 13, 2016
2 parents de7247c + 27ef3be commit 16fe5e0
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 56 deletions.
6 changes: 3 additions & 3 deletions client/dist/js/bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/lang/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
"AssetAdmin.DELETE": "Delete",
"AssetAdmin.DIM": "Dimensions",
"AssetAdmin.DROPZONE_CANCEL_UPLOAD": "Cancel upload",
"AssetAdmin.DROPZONE_CANNOT_UPLOAD": "You can't upload files here",
"AssetAdmin.DROPZONE_CANCEL_UPLOAD_CONFIRMATION": "Are you sure you want to cancel this upload?",
"AssetAdmin.DROPZONE_DEFAULT_MESSAGE": "Drop files here to upload",
"AssetAdmin.DROPZONE_FAILED_UPLOAD": "Failed to upload file",
Expand Down
129 changes: 82 additions & 47 deletions client/src/components/AssetDropzone/AssetDropzone.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ class AssetDropzone extends SilverStripeComponent {

this.dropzone = null;
this.dragging = false;

this.handleAddedFile = this.handleAddedFile.bind(this);
this.handleDragEnter = this.handleDragEnter.bind(this);
this.handleDragLeave = this.handleDragLeave.bind(this);
this.handleDrop = this.handleDrop.bind(this);
this.handleUploadProgress = this.handleUploadProgress.bind(this);
this.handleError = this.handleError.bind(this);
this.handleSending = this.handleSending.bind(this);
this.handleSuccess = this.handleSuccess.bind(this);
}

componentDidMount() {
Expand Down Expand Up @@ -86,19 +95,19 @@ class AssetDropzone extends SilverStripeComponent {

// By default Dropzone adds markup to the DOM for displaying a thumbnail preview.
// Here we're relpacing that default behaviour with our own React / Redux implementation.
addedfile: this.handleAddedFile.bind(this),
addedfile: this.handleAddedFile,

// When the user drags a file into the dropzone.
dragenter: this.handleDragEnter.bind(this),
dragenter: this.handleDragEnter,

// When the user's cursor leaves the dropzone while dragging a file.
dragleave: this.handleDragLeave.bind(this),
dragleave: this.handleDragLeave,

// When the user drops a file onto the dropzone.
drop: this.handleDrop.bind(this),
drop: this.handleDrop,

// Whenever the file upload progress changes
uploadprogress: this.handleUploadProgress.bind(this),
uploadprogress: this.handleUploadProgress,

// The text used before any files are dropped
dictDefaultMessage: i18n._t('AssetAdmin.DROPZONE_DEFAULT_MESSAGE'),
Expand Down Expand Up @@ -132,13 +141,13 @@ class AssetDropzone extends SilverStripeComponent {
dictMaxFilesExceeded: i18n._t('AssetAdmin.DROPZONE_MAX_FILES_EXCEEDED'),

// When a file upload fails.
error: this.handleError.bind(this),
error: this.handleError,

// When file file is sent to the server.
sending: this.handleSending.bind(this),
sending: this.handleSending,

// When a file upload succeeds.
success: this.handleSuccess.bind(this),
success: this.handleSuccess,

thumbnailHeight: 150,

Expand Down Expand Up @@ -250,72 +259,98 @@ class AssetDropzone extends SilverStripeComponent {
/**
* Event handler for files being added. Called before the request is made to the server.
*
* @param object file - File interface. See https://developer.mozilla.org/en-US/docs/Web/API/File
* @param file (object) - File interface. See https://developer.mozilla.org/en-US/docs/Web/API/File
*/
handleAddedFile(file) {
if (!this.props.canUpload) {
return;
return Promise.reject(new Error(i18n._t('AssetAdmin.DROPZONE_CANNOT_UPLOAD')));
}

const reader = new FileReader();

// The queuedAtTime is used to uniquely identify file while it's in the queue.
const queuedAtTime = Date.now();

reader.onload = (event) => {
// If the user uploads multiple large images, we could run into memory issues
// by simply using the `event.target.result` data URI as the thumbnail image.
//
// To get avoid this we're creating a canvas, using the dropzone thumbnail dimensions,
// and using the canvas data URI as the thumbnail image instead.

let thumbnailURL = '';
// eslint-disable-next-line no-param-reassign
file._queuedAtTime = Date.now();

if (this.getFileCategory(file.type) === 'image') {
const img = document.createElement('img');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const loadPreview = new Promise((resolve) => {
const reader = new FileReader();

img.src = event.target.result;
reader.onload = (event) => {
// If the user uploads multiple large images, we could run into memory issues
// by simply using the `event.target.result` data URI as the thumbnail image.
//
// To get avoid this we're creating a canvas, using the dropzone thumbnail dimensions,
// and using the canvas data URI as the thumbnail image instead.

canvas.width = this.dropzone.options.thumbnailWidth;
canvas.height = this.dropzone.options.thumbnailHeight;
if (this.getFileCategory(file.type) === 'image') {
const img = document.createElement('img');

ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
resolve(this.loadImage(img, event.target.result));
} else {
resolve({});
}
};

thumbnailURL = canvas.toDataURL();
}
reader.readAsDataURL(file);
});

this.props.handleAddedFile({
attributes: {
dimensions: {
height: this.dropzone.options.thumbnailHeight,
width: this.dropzone.options.thumbnailWidth,
},
return loadPreview.then((preview) => {
const details = {
dimensions: {
height: preview.height,
width: preview.width,
},
category: this.getFileCategory(file.type),
filename: file.name,
queuedAtTime,
queuedAtTime: file._queuedAtTime,
size: file.size,
title: file.name,
type: file.type,
url: thumbnailURL,
});
url: preview.thumbnailURL,
};

this.props.handleAddedFile(details);
this.dropzone.processFile(file);
};

// eslint-disable-next-line no-param-reassign
file._queuedAtTime = queuedAtTime;
return details;
});
}

/**
* Returns a promise for loading an image to get the dataURL for previewing.
*
* @param img (image)
* @param newSource (string)
* @returns {Promise}
*/
loadImage(img, newSource) {
return new Promise((resolve) => {
// eslint-disable-next-line no-param-reassign
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;

reader.readAsDataURL(file);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

const thumbnailURL = canvas.toDataURL();

resolve({
width: canvas.width,
height: canvas.height,
thumbnailURL,
});
};
// eslint-disable-next-line no-param-reassign
img.src = newSource;
});
}

/**
* Event handler for failed uploads.
*
* @param object file - File interface. See https://developer.mozilla.org/en-US/docs/Web/API/File
* @param string errorMessage
* @param file (object) - File interface. See https://developer.mozilla.org/en-US/docs/Web/API/File
* @param errorMessage (string)
*/
handleError(file, errorMessage) {
if (typeof this.props.handleSending === 'function') {
Expand Down
53 changes: 53 additions & 0 deletions client/src/components/AssetDropzone/tests/AssetDropzone-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,59 @@ describe('AssetDropzone', () => {
});

describe('handleAddedFile()', () => {
let item;
let uploadProps;

beforeEach(() => {
uploadProps = Object.assign({}, props, {
handleAddedFile: jest.genMockFunction(),
});
});

it('restricts uploading', (done) => {
uploadProps.canUpload = false;

item = ReactTestUtils.renderIntoDocument(
<AssetDropzone {...uploadProps} />
);

return item.handleAddedFile({})
.then(() => {
expect("This shouldn't be called").toBeFalsey();
})
.catch((error) => {
expect(error instanceof Error).toBeTruthy();
})
.then(() => done());
});

it('loads non-images', (done) => {
const file = {
size: 123,
name: 'Test file',
type: 'text/plain',
};

item = ReactTestUtils.renderIntoDocument(
<AssetDropzone {...uploadProps} />
);
item.dropzone = {
processFile: jest.genMockFunction(),
};

return item.handleAddedFile(file)
.then((details) => {
expect(uploadProps.handleAddedFile).toBeCalled();
expect(item.dropzone.processFile).toBeCalled();
expect(details.size).toBe(123);
expect(details.title).toBe('Test file');
expect(details.url).toBeUndefined();
})
.catch(() => {
expect("This shouldn't be called").toBeFalsey();
})
.then(() => done());
});
});

describe('setPromptOnRemove()', () => {
Expand Down
19 changes: 13 additions & 6 deletions client/src/components/GalleryItem/GalleryItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class GalleryItem extends SilverStripeComponent {
* @returns {Object}
*/
getThumbnailStyles() {
if (this.isImage() && this.exists()) {
if (this.isImage() && (this.exists() || this.uploading())) {
return {
backgroundImage: `url(${this.props.item.url})`,
};
Expand Down Expand Up @@ -117,7 +117,7 @@ class GalleryItem extends SilverStripeComponent {
getItemClassNames() {
const itemClassNames = [`gallery-item gallery-item--${this.props.item.category}`];

if (!this.exists()) {
if (!this.exists() && !this.uploading()) {
itemClassNames.push('gallery-item--error');
}

Expand Down Expand Up @@ -154,13 +154,17 @@ class GalleryItem extends SilverStripeComponent {
return this.props.item.exists;
}

uploading() {
return this.props.uploading;
}

/**
* Determine that this record is an image, and the thumbnail is smaller than the given thumbnail area
*
* @returns {boolean}
*/
isImageSmallerThanThumbnail() {
if (!this.isImage() || !this.exists()) {
if (!this.isImage() || (!this.exists() && !this.uploading())) {
return false;
}
const dimensions = this.props.item.dimensions;
Expand Down Expand Up @@ -225,7 +229,7 @@ class GalleryItem extends SilverStripeComponent {
},
};

if (!this.hasError() && this.props.uploading) {
if (!this.hasError() && this.uploading()) {
progressBar = (
<div className="gallery-item__upload-progress">
<div {...progressBarProps}></div>
Expand All @@ -238,8 +242,9 @@ class GalleryItem extends SilverStripeComponent {

render() {
let actionInputCheckbox;
let overlay;

if (this.props.uploading) {
if (this.uploading()) {
actionInputCheckbox = (<label
className="gallery-item__checkbox-label font-icon-cancel"
onClick={this.handleCancelUpload}
Expand All @@ -264,6 +269,8 @@ class GalleryItem extends SilverStripeComponent {
tabIndex="-1"
onMouseDown={this.preventFocus}
/></label>);

overlay = <div className="gallery-item--overlay font-icon-edit">View</div>;
}

return (
Expand All @@ -279,7 +286,7 @@ class GalleryItem extends SilverStripeComponent {
className={this.getThumbnailClassNames()}
style={this.getThumbnailStyles()}
>
<div className="gallery-item--overlay font-icon-edit">View</div>
{overlay}
</div>
{this.getProgressBar()}
{this.getErrorMessage()}
Expand Down
1 change: 1 addition & 0 deletions client/src/containers/Gallery/Gallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export class Gallery extends Component {
}

handleCancelUpload(fileData) {
// abort wasn't defined..?
fileData.xhr.abort();
this.props.actions.queuedFiles.removeQueuedFile(fileData.queuedAtTime);
}
Expand Down

0 comments on commit 16fe5e0

Please sign in to comment.