REFACTOR: uploading avatar should share code with upload component

This commit is contained in:
Robin Ward 2014-06-26 17:07:12 -04:00
parent 4088fba4f2
commit 3cbb32cc20
7 changed files with 104 additions and 141 deletions

View file

@ -0,0 +1,30 @@
import UploadMixin from 'discourse/mixins/upload';
export default Em.Component.extend(UploadMixin, {
tagName: 'span',
imageIsNotASquare: false,
type: 'avatar',
uploadUrl: Discourse.computed.url('username', '/users/%@/preferences/user_image'),
uploadButtonText: function() {
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
}.property("uploading"),
uploadDone: function(data) {
var self = this;
// indicates the users is using an uploaded avatar
this.set("custom_avatar_upload_id", data.result.upload_id);
// display a warning whenever the image is not a square
this.set("imageIsNotASquare", data.result.width !== data.result.height);
// in order to be as much responsive as possible, we're cheating a bit here
// indeed, the server gives us back the url to the file we've just uploaded
// often, this file is not a square, so we need to crop it properly
// this will also capture the first frame of animated avatars when they're not allowed
Discourse.Utilities.cropAvatar(data.result.url, data.files[0].type).then(function(avatarTemplate) {
self.set("uploadedAvatarTemplate", avatarTemplate);
});
}
});

View file

@ -1,6 +1,6 @@
export default Em.Component.extend({ import UploadMixin from 'discourse/mixins/upload';
uploading: false,
uploadProgress: 0, export default Em.Component.extend(UploadMixin, {
backgroundStyle: function() { backgroundStyle: function() {
var imageUrl = this.get('imageUrl'); var imageUrl = this.get('imageUrl');
@ -9,50 +9,11 @@ export default Em.Component.extend({
return "background-image: url(" + imageUrl + ")"; return "background-image: url(" + imageUrl + ")";
}.property('imageUrl'), }.property('imageUrl'),
_initializeUploader: function() { uploadDone: function(data) {
var $upload = this.$('input[type=file]'), // note: we can't cache this as fileupload replaces the input after upload this.set('imageUrl', data.result.url);
self = this; },
$upload.fileupload({
url: this.get('uploadUrl'),
dataType: "json",
fileInput: $upload,
formData: { image_type: this.get('type') }
});
$upload.on('fileuploadsubmit', function (e, data) {
var result = Discourse.Utilities.validateUploadedFiles(data.files, true);
self.setProperties({ uploadProgress: 0, uploading: result });
return result;
});
$upload.on("fileuploadprogressall", function(e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
self.set("uploadProgress", progress);
});
$upload.on("fileuploaddone", function(e, data) {
if(data.result.url) {
self.set('imageUrl', data.result.url);
} else {
bootbox.alert(I18n.t('post.errors.upload'));
}
});
$upload.on("fileuploadfail", function(e, data) {
Discourse.Utilities.displayErrorForUpload(data);
});
$upload.on("fileuploadalways", function() {
self.setProperties({ uploading: false, uploadProgress: 0});
});
}.on('didInsertElement'),
_destroyUploader: function() {
this.$('input[type=file]').fileupload('destroy');
}.on('willDestroyElement'),
actions: { actions: {
selectFile: function() {
this.$('input[type=file]').click();
},
trash: function() { trash: function() {
this.set('imageUrl', null); this.set('imageUrl', null);
this.sendAction('clear'); this.sendAction('clear');

View file

@ -0,0 +1,54 @@
export default Em.Mixin.create({
uploading: false,
uploadProgress: 0,
uploadDone: function() {
Em.warn("You should implement `uploadDone`");
},
_initializeUploader: function() {
var $upload = this.$('input[type=file]'), // note: we can't cache this as fileupload replaces the input after upload
self = this;
$upload.fileupload({
url: this.get('uploadUrl'),
dataType: "json",
fileInput: $upload,
formData: { image_type: this.get('type') },
pasteZone: this.$()
});
$upload.on('fileuploadsubmit', function (e, data) {
var result = Discourse.Utilities.validateUploadedFiles(data.files, true);
self.setProperties({ uploadProgress: 0, uploading: result });
return result;
});
$upload.on("fileuploadprogressall", function(e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
self.set("uploadProgress", progress);
});
$upload.on("fileuploaddone", function(e, data) {
if(data.result.url) {
self.uploadDone(data);
} else {
bootbox.alert(I18n.t('post.errors.upload'));
}
});
$upload.on("fileuploadfail", function(e, data) {
Discourse.Utilities.displayErrorForUpload(data);
});
$upload.on("fileuploadalways", function() {
self.setProperties({ uploading: false, uploadProgress: 0});
});
}.on('didInsertElement'),
_destroyUploader: function() {
this.$('input[type=file]').fileupload('destroy');
}.on('willDestroyElement'),
actions: {
selectFile: function() {
this.$('input[type=file]').click();
}
}
});

View file

@ -350,7 +350,6 @@ Discourse.User = Discourse.Model.extend({
@returns {Promise} the result of the clear profile background request @returns {Promise} the result of the clear profile background request
*/ */
clearProfileBackground: function() { clearProfileBackground: function() {
var user = this;
return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/profile_background/clear", { return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/profile_background/clear", {
type: 'PUT', type: 'PUT',
data: { } data: { }

View file

@ -0,0 +1,10 @@
<input type="file" accept="image/*" style="display:none" />
<button class="btn" {{action selectFile}} {{bind-attr disabled="uploading"}} title="{{i18n user.change_avatar.upload_title}}">
<i class="fa fa-picture-o"></i>&nbsp;{{uploadButtonText}}
</button>
{{#if uploading}}
<span>{{i18n upload_selector.uploading}} {{view.uploadProgress}}%</span>
{{/if}}
{{#if imageIsNotASquare}}
<div class="warning">{{i18n user.change_avatar.image_is_not_a_square}}</div>
{{/if}}

View file

@ -7,7 +7,7 @@
<div> <div>
<input type="radio" id="gravatar" name="avatar" value="gravatar" {{action useGravatar}}> <input type="radio" id="gravatar" name="avatar" value="gravatar" {{action useGravatar}}>
<label class="radio" for="gravatar">{{bound-avatar controller "large" gravatar_avatar_upload_id}} {{{i18n user.change_avatar.gravatar}}} {{email}}</label> <label class="radio" for="gravatar">{{bound-avatar controller "large" gravatar_avatar_upload_id}} {{{i18n user.change_avatar.gravatar}}} {{email}}</label>
<button href="#" {{action refreshGravatar}} title="{{i18n user.change_avatar.refresh_gravatar_title}}" {{bind-attr enabled="view.gravatarRefreshEnabled"}} class="btn no-text"><i class="fa fa-refresh"></i></button> <button href {{action refreshGravatar}} title="{{i18n user.change_avatar.refresh_gravatar_title}}" {{bind-attr enabled="view.gravatarRefreshEnabled"}} class="btn no-text"><i class="fa fa-refresh"></i></button>
</div> </div>
<div> <div>
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action useUploadedAvatar}}> <input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action useUploadedAvatar}}>
@ -22,16 +22,9 @@
{{i18n user.change_avatar.uploaded_avatar_empty}} {{i18n user.change_avatar.uploaded_avatar_empty}}
{{/if}} {{/if}}
</label> </label>
<button id="fake-avatar-input" class="btn" {{bind-attr disabled="view.uploading"}} title="{{i18n user.change_avatar.upload_title}}"> {{avatar-uploader username=username
<i class="fa fa-picture-o"></i>&nbsp;{{view.uploadButtonText}} uploadedAvatarTemplate=view.uploadedAvatarTemplate
</button> custom_avatar_upload_id=controller.custom_avatar_upload_id}}
<input type="file" id="avatar-input" accept="image/*" style="display:none">
{{#if view.uploading}}
<span>{{i18n upload_selector.uploading}} {{view.uploadProgress}}%</span>
{{/if}}
{{#if view.imageIsNotASquare}}
<div class="warning">{{i18n user.change_avatar.image_is_not_a_square}}</div>
{{/if}}
</div> </div>
</div> </div>
</div> </div>

View file

@ -10,90 +10,10 @@ Discourse.AvatarSelectorView = Discourse.ModalBodyView.extend({
templateName: 'modal/avatar_selector', templateName: 'modal/avatar_selector',
classNames: ['avatar-selector'], classNames: ['avatar-selector'],
title: I18n.t('user.change_avatar.title'), title: I18n.t('user.change_avatar.title'),
uploading: false,
uploadProgress: 0,
saveDisabled: false, saveDisabled: false,
gravatarRefreshEnabled: Em.computed.not('controller.gravatarRefreshDisabled'), gravatarRefreshEnabled: Em.computed.not('controller.gravatarRefreshDisabled'),
imageIsNotASquare : false,
hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'controller.custom_avatar_upload_id'), hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'controller.custom_avatar_upload_id'),
didInsertElement: function() {
var self = this;
var $upload = $("#avatar-input");
this._super();
// simulate a click on the hidden file input when clicking on our fake file input
$("#fake-avatar-input").on("click", function(e) {
// do *NOT* use the cached `$upload` variable, because fileupload is cloning & replacing the input
// cf. https://github.com/blueimp/jQuery-File-Upload/wiki/Frequently-Asked-Questions#why-is-the-file-input-field-cloned-and-replaced-after-each-selection
$("#avatar-input").click();
e.preventDefault();
});
// define the upload endpoint
$upload.fileupload({
url: Discourse.getURL("/users/" + this.get("controller.username") + "/preferences/user_image"),
dataType: "json",
fileInput: $upload,
formData: { image_type: "avatar" }
});
// when a file has been selected
$upload.on('fileuploadsubmit', function (e, data) {
var result = Discourse.Utilities.validateUploadedFiles(data.files, true);
self.setProperties({
uploadProgress: 0,
uploading: result,
imageIsNotASquare: false
});
return result;
});
// when there is a progression for the upload
$upload.on("fileuploadprogressall", function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
self.set("uploadProgress", progress);
});
// when the upload is successful
$upload.on("fileuploaddone", function (e, data) {
// make sure we have a url
if (data.result.url) {
// indicates the users is using an uploaded avatar
self.set("controller.custom_avatar_upload_id", data.result.upload_id);
// display a warning whenever the image is not a square
self.set("imageIsNotASquare", data.result.width !== data.result.height);
// in order to be as much responsive as possible, we're cheating a bit here
// indeed, the server gives us back the url to the file we've just uploaded
// often, this file is not a square, so we need to crop it properly
// this will also capture the first frame of animated avatars when they're not allowed
Discourse.Utilities.cropAvatar(data.result.url, data.files[0].type).then(function(avatarTemplate) {
self.set("uploadedAvatarTemplate", avatarTemplate);
});
} else {
bootbox.alert(I18n.t('post.errors.upload'));
}
});
// when there has been an error with the upload
$upload.on("fileuploadfail", function (e, data) {
Discourse.Utilities.displayErrorForUpload(data);
});
// when the upload is done
$upload.on("fileuploadalways", function () {
self.setProperties({ uploading: false, uploadProgress: 0 });
});
},
willDestroyElement: function() {
$("#fake-avatar-input").off("click");
$("#avatar-input").fileupload("destroy");
},
// *HACK* used to select the proper radio button, cause {{action}} // *HACK* used to select the proper radio button, cause {{action}}
// stops the default behavior // stops the default behavior
selectedChanged: function() { selectedChanged: function() {
@ -104,8 +24,4 @@ Discourse.AvatarSelectorView = Discourse.ModalBodyView.extend({
}); });
}.observes('controller.selected'), }.observes('controller.selected'),
uploadButtonText: function() {
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
}.property("uploading")
}); });