Remove usage of jquery-file-upload (#815)

closes https://github.com/TryGhost/Ghost/issues/6661
- refactor `gh-profile-image` component to use native browser functionality
- remove `jquery-file-upload` bower dependency
This commit is contained in:
Kevin Ansfield 2017-08-18 04:27:42 +01:00 committed by Aileen Nowak
parent d860403b93
commit 327cbdf7a2
6 changed files with 126 additions and 94 deletions

View File

@ -1,7 +1,7 @@
import $ from 'jquery';
import Component from 'ember-component';
import injectService from 'ember-service/inject';
import request from 'ember-ajax/request';
import run from 'ember-runloop';
import {htmlSafe} from 'ember-string';
import {task, timeout} from 'ember-concurrency';
@ -26,8 +26,12 @@ export default Component.extend({
size: 180,
debounce: 300,
imageFile: null,
hasUploadedImage: false,
// closure actions
setImage() {},
config: injectService(),
ghostPaths: injectService(),
@ -43,28 +47,6 @@ export default Component.extend({
this._setPlaceholderImage(this._defaultImageUrl);
},
didInsertElement() {
this._super(...arguments);
let size = this.get('size');
let uploadElement = this.$('.js-file-input');
// while theoretically the 'add' and 'processalways' functions could be
// added as properties of the hash passed to fileupload(), for some reason
// they needed to be placed in an on() call for the add method to work correctly
uploadElement.fileupload({
url: this.get('ghostPaths.url').api('uploads'),
dropZone: this.$('.js-img-dropzone'),
previewMaxHeight: size,
previewMaxWidth: size,
previewCrop: true,
maxNumberOfFiles: 1,
autoUpload: false
})
.on('fileuploadadd', run.bind(this, this.queueFile))
.on('fileuploadprocessalways', run.bind(this, this.triggerPreview));
},
didReceiveAttrs() {
this._super(...arguments);
@ -73,6 +55,32 @@ export default Component.extend({
}
},
dragOver(event) {
if (!event.dataTransfer) {
return;
}
// this is needed to work around inconsistencies with dropping files
// from Chrome's downloads bar
let eA = event.dataTransfer.effectAllowed;
event.dataTransfer.dropEffect = (eA === 'move' || eA === 'linkMove') ? 'move' : 'copy';
event.stopPropagation();
event.preventDefault();
},
dragLeave(event) {
event.preventDefault();
},
drop(event) {
event.preventDefault();
if (event.dataTransfer.files) {
this.send('imageSelected', event.dataTransfer.files);
}
},
setGravatar: task(function* () {
yield timeout(this.get('debounce'));
@ -110,16 +118,6 @@ export default Component.extend({
this.set('avatarStyle', htmlSafe(`background-image: url(${url}); display: ${display}`));
},
willDestroyElement() {
let $input = this.$('.js-file-input');
this._super(...arguments);
if ($input.length && $input.data()['blueimp-fileupload']) {
$input.fileupload('destroy');
}
},
queueFile(e, data) {
let fileName = data.files[0].name;
@ -128,15 +126,40 @@ export default Component.extend({
}
},
triggerPreview(e, data) {
let file = data.files[data.index];
actions: {
imageSelected(fileList) {
// eslint-disable-next-line
let imageFile = fileList[0];
if (file.preview) {
this.set('hasUploadedImage', true);
// necessary jQuery code because file.preview is a raw DOM object
// potential todo: rename 'gravatar-img' class in the CSS to be something
// that both the gravatar and the image preview can use that's not so confusing
this.$('.js-img-preview').empty().append(this.$(file.preview).addClass('gravatar-img'));
if (imageFile) {
let reader = new FileReader();
this.set('imageFile', imageFile);
this.setImage(imageFile);
reader.addEventListener('load', () => {
let dataURL = reader.result;
this.set('previewDataURL', dataURL);
}, false);
reader.readAsDataURL(imageFile);
}
},
openFileDialog(event) {
let fileInput = $(event.target)
.closest('figure')
.find('input[type="file"]');
if (fileInput.length > 0) {
// reset file input value before clicking so that the same image
// can be selected again
fileInput.value = '';
// simulate click to open file dialog
// using jQuery because IE11 doesn't support MouseEvent
$(fileInput).click();
}
}
}
});

View File

@ -8,8 +8,6 @@ import {isInvalidError} from 'ember-ajax/errors';
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
import {task} from 'ember-concurrency';
const {Promise} = RSVP;
export default Controller.extend(ValidationEngine, {
ajax: injectService(),
application: injectController(),
@ -95,22 +93,27 @@ export default Controller.extend(ValidationEngine, {
* @return {Ember.RSVP.Promise} A promise that takes care of both calls
*/
_sendImage(user) {
let image = this.get('profileImage');
let formData = new FormData();
let imageFile = this.get('profileImage');
let uploadUrl = this.get('ghostPaths.url').api('uploads');
return new Promise((resolve, reject) => {
image.formData = {};
return image.submit()
.done((response) => {
let usersUrl = this.get('ghostPaths.url').api('users', user.id.toString());
user.profile_image = response;
formData.append('uploadimage', imageFile, imageFile.name);
return this.get('ajax').put(usersUrl, {
data: {
users: [user]
}
}).then(resolve).catch(reject);
})
.fail(reject);
return this.get('ajax').post(uploadUrl, {
data: formData,
processData: false,
contentType: false,
dataType: 'text'
}).then((response) => {
let imageUrl = JSON.parse(response);
let usersUrl = this.get('ghostPaths.url').api('users', user.id.toString());
user.profile_image = imageUrl;
return this.get('ajax').put(usersUrl, {
data: {
users: [user]
}
});
});
},
@ -225,19 +228,17 @@ export default Controller.extend(ValidationEngine, {
return this._sendImage(result.users[0])
.then(() => {
// fetch settings and private config for synchronous access before transitioning
return RSVP.all(promises)
.then(() => {
return this.transitionToRoute('setup.three');
});
return RSVP.all(promises).then(() => {
return this.transitionToRoute('setup.three');
});
}).catch((resp) => {
this.get('notifications').showAPIError(resp, {key: 'setup.blog-details'});
});
} else {
// fetch settings and private config for synchronous access before transitioning
return RSVP.all(promises)
.then(() => {
return this.transitionToRoute('setup.three');
});
return RSVP.all(promises).then(() => {
return this.transitionToRoute('setup.three');
});
}
},

View File

@ -10,8 +10,6 @@ import {assign} from 'ember-platform';
import {isEmberArray} from 'ember-array/utils';
import {task} from 'ember-concurrency';
const {Promise} = RSVP;
export default Controller.extend(ValidationEngine, {
ajax: injectService(),
config: injectService(),
@ -25,7 +23,7 @@ export default Controller.extend(ValidationEngine, {
validationType: 'signup',
flowErrors: '',
image: null,
profileImage: null,
authenticate: task(function* (authStrategy, authentication) {
try {
@ -154,23 +152,30 @@ export default Controller.extend(ValidationEngine, {
},
_sendImage() {
let image = this.get('image');
let formData = new FormData();
let imageFile = this.get('profileImage');
let uploadUrl = this.get('ghostPaths.url').api('uploads');
if (imageFile) {
formData.append('uploadimage', imageFile, imageFile.name);
if (image) {
return this.get('session.user').then((user) => {
return new Promise((resolve, reject) => {
image.formData = {};
return image.submit()
.done((response) => {
let usersUrl = this.get('ghostPaths.url').api('users', user.id.toString());
user.image = response;
return this.get('ajax').put(usersUrl, {
data: {
users: [user]
}
}).then(resolve).catch(reject);
})
.fail(reject);
return this.get('ajax').post(uploadUrl, {
data: formData,
processData: false,
contentType: false,
dataType: 'text'
}).then((response) => {
let imageUrl = JSON.parse(response);
let usersUrl = this.get('ghostPaths.url').api('users', user.id.toString());
// eslint-disable-next-line
user.profile_image = imageUrl;
return this.get('ajax').put(usersUrl, {
data: {
users: [user]
}
});
});
});
}

View File

@ -1,16 +1,25 @@
<figure class="account-image js-file-upload">
{{#unless hasUploadedImage}}
<figure class="account-image">
{{#unless previewDataURL}}
<div class="placeholder-img" style={{placeholderStyle}}></div>
<div id="account-image" class="gravatar-img" style={{avatarStyle}}>
<span class="sr-only">User image</span>
</div>
{{/unless}}
<div class="js-img-preview"></div>
{{#if previewDataURL}}
<img src={{previewDataURL}} class="gravatar-img">
{{/if}}
<span class="edit-account-image js-img-dropzone">
<span class="edit-account-image" onclick={{action "openFileDialog"}} role="button">
{{inline-svg "photos"}}
<span class="sr-only">Upload an image</span>
</span>
<input type="file" class="file-uploader js-file-input" name="uploadimage">
{{gh-file-input
alt=null
name="uploadimage"
multiple=false
action=(action "imageSelected")
accept=imageMimeTypes}}
</figure>

View File

@ -4,7 +4,6 @@
"devicejs": "0.2.7",
"Faker": "3.1.0",
"google-caja": "6005.0.0",
"jquery-file-upload": "9.12.3",
"jquery-ui": "1.11.4",
"jquery.simulate.drag-sortable": "0.1.0",
"jqueryui-touch-punch": "furf/jquery-ui-touch-punch#4bc009145202d9c7483ba85f3a236a8f3470354d",

View File

@ -186,11 +186,6 @@ module.exports = function (defaults) {
app.import('bower_components/jquery-ui/ui/draggable.js');
app.import('bower_components/jquery-ui/ui/droppable.js');
app.import('bower_components/jquery-ui/ui/sortable.js');
app.import('bower_components/jquery-file-upload/js/jquery.fileupload.js');
app.import('bower_components/blueimp-load-image/js/load-image.all.min.js');
app.import('bower_components/jquery-file-upload/js/jquery.fileupload-process.js');
app.import('bower_components/jquery-file-upload/js/jquery.fileupload-image.js');
app.import('bower_components/google-caja/html-css-sanitizer-bundle.js');
app.import('bower_components/jqueryui-touch-punch/jquery.ui.touch-punch.js');