From 327cbdf7a29cf02f56d269cade12fbf2a6b248d6 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 18 Aug 2017 04:27:42 +0100 Subject: [PATCH] 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 --- .../admin/app/components/gh-profile-image.js | 105 +++++++++++------- ghost/admin/app/controllers/setup/two.js | 49 ++++---- ghost/admin/app/controllers/signup.js | 41 ++++--- .../templates/components/gh-profile-image.hbs | 19 +++- ghost/admin/bower.json | 1 - ghost/admin/ember-cli-build.js | 5 - 6 files changed, 126 insertions(+), 94 deletions(-) diff --git a/ghost/admin/app/components/gh-profile-image.js b/ghost/admin/app/components/gh-profile-image.js index c3547713da..2af64bd3e5 100644 --- a/ghost/admin/app/components/gh-profile-image.js +++ b/ghost/admin/app/components/gh-profile-image.js @@ -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(); + } } } }); diff --git a/ghost/admin/app/controllers/setup/two.js b/ghost/admin/app/controllers/setup/two.js index a370d43f7f..243ba7f7d1 100644 --- a/ghost/admin/app/controllers/setup/two.js +++ b/ghost/admin/app/controllers/setup/two.js @@ -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'); + }); } }, diff --git a/ghost/admin/app/controllers/signup.js b/ghost/admin/app/controllers/signup.js index 432031bdf1..ae8148624a 100644 --- a/ghost/admin/app/controllers/signup.js +++ b/ghost/admin/app/controllers/signup.js @@ -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] + } + }); }); }); } diff --git a/ghost/admin/app/templates/components/gh-profile-image.hbs b/ghost/admin/app/templates/components/gh-profile-image.hbs index a0d79e031b..0bb9851762 100644 --- a/ghost/admin/app/templates/components/gh-profile-image.hbs +++ b/ghost/admin/app/templates/components/gh-profile-image.hbs @@ -1,16 +1,25 @@ -
- {{#unless hasUploadedImage}} + diff --git a/ghost/admin/bower.json b/ghost/admin/bower.json index 49b9a34e10..116df07469 100644 --- a/ghost/admin/bower.json +++ b/ghost/admin/bower.json @@ -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", diff --git a/ghost/admin/ember-cli-build.js b/ghost/admin/ember-cli-build.js index ef1760046d..eebedcf664 100644 --- a/ghost/admin/ember-cli-build.js +++ b/ghost/admin/ember-cli-build.js @@ -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');