Fix #7959: Fix avatar stretching by providing a cropping feature
This commit is contained in:
parent
16abacb1f0
commit
6d5808801f
3
Gemfile
3
Gemfile
|
|
@ -77,6 +77,9 @@ gem "haml-rails", '~> 0.9.0'
|
|||
# Files attachments
|
||||
gem "carrierwave", '~> 0.9.0'
|
||||
|
||||
# Image editing
|
||||
gem "mini_magick", '~> 4.4.0'
|
||||
|
||||
# Drag and Drop UI
|
||||
gem 'dropzonejs-rails', '~> 0.7.1'
|
||||
|
||||
|
|
|
|||
|
|
@ -468,6 +468,7 @@ GEM
|
|||
method_source (0.8.2)
|
||||
mime-types (1.25.1)
|
||||
mimemagic (0.3.0)
|
||||
mini_magick (4.4.0)
|
||||
mini_portile2 (2.0.0)
|
||||
minitest (5.7.0)
|
||||
mousetrap-rails (1.4.6)
|
||||
|
|
@ -954,6 +955,7 @@ DEPENDENCIES
|
|||
loofah (~> 2.0.3)
|
||||
mail_room (~> 0.6.1)
|
||||
method_source (~> 0.8)
|
||||
mini_magick (~> 4.4.0)
|
||||
minitest (~> 5.7.0)
|
||||
mousetrap-rails (~> 1.4.6)
|
||||
mysql2 (~> 0.3.16)
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
#= require jquery.nicescroll
|
||||
#= require_tree .
|
||||
#= require fuzzaldrin-plus
|
||||
#= require cropper.js
|
||||
|
||||
window.slugify = (text) ->
|
||||
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
|
||||
|
|
@ -210,7 +211,7 @@ $ ->
|
|||
$this = $(this)
|
||||
$this.attr 'value', $this.val()
|
||||
return
|
||||
|
||||
|
||||
$(document)
|
||||
.off 'keyup', 'input[type="search"]'
|
||||
.on 'keyup', 'input[type="search"]' , (e) ->
|
||||
|
|
@ -253,7 +254,7 @@ $ ->
|
|||
$('.page-with-sidebar')
|
||||
.removeClass('right-sidebar-collapsed')
|
||||
.addClass('right-sidebar-expanded')
|
||||
$.cookie("collapsed_gutter",
|
||||
$.cookie("collapsed_gutter",
|
||||
$('.right-sidebar')
|
||||
.hasClass('right-sidebar-collapsed'), { path: '/' })
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,51 @@ class @Profile
|
|||
$('.update-notifications').on 'ajax:complete', ->
|
||||
$(this).find('.btn-save').enable()
|
||||
|
||||
$('.js-choose-user-avatar-button').bind "click", ->
|
||||
form = $(this).closest("form")
|
||||
form.find(".js-user-avatar-input").click()
|
||||
# Avatar management
|
||||
|
||||
$('.js-user-avatar-input').bind "change", ->
|
||||
$avatarInput = $('.js-user-avatar-input')
|
||||
$filename = $('.js-avatar-filename')
|
||||
$modalCrop = $('.modal-profile-crop')
|
||||
$modalCropImg = $('.modal-profile-crop-image')
|
||||
|
||||
$('.js-choose-user-avatar-button').on "click", ->
|
||||
$form = $(this).closest("form")
|
||||
$form.find(".js-user-avatar-input").click()
|
||||
|
||||
$modalCrop.on 'shown.bs.modal', ->
|
||||
setTimeout ( -> # The cropper must be asynchronously initialized
|
||||
$modalCropImg.cropper
|
||||
aspectRatio: 1
|
||||
autoCropArea: 1
|
||||
modal: false
|
||||
scalable: false
|
||||
rotatable: false
|
||||
zoomable: false
|
||||
|
||||
crop: (event) ->
|
||||
['x', 'y'].forEach (key) ->
|
||||
$("#user_avatar_crop_#{key}").val(Math.floor(event[key]))
|
||||
$("#user_avatar_crop_size").val(Math.floor(event.width))
|
||||
), 0
|
||||
|
||||
$modalCrop.on 'hidden.bs.modal', ->
|
||||
$modalCropImg.attr('src', '').cropper('destroy')
|
||||
$avatarInput.val('')
|
||||
$filename.text($filename.data('label'))
|
||||
|
||||
$('.js-upload-user-avatar').on 'click', ->
|
||||
$('.edit_user').submit()
|
||||
|
||||
$avatarInput.on "change", ->
|
||||
form = $(this).closest("form")
|
||||
filename = $(this).val().replace(/^.*[\\\/]/, '')
|
||||
form.find(".js-avatar-filename").text(filename)
|
||||
$filename.text(filename)
|
||||
|
||||
reader = new FileReader
|
||||
|
||||
reader.onload = (event) ->
|
||||
$modalCrop.modal('show')
|
||||
$modalCropImg.attr('src', event.target.result)
|
||||
|
||||
fileData = reader.readAsDataURL(this.files[0])
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
*= require_self
|
||||
*= require dropzone/basic
|
||||
*= require cal-heatmap
|
||||
*= require cropper.css
|
||||
*/
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@
|
|||
transition: $transition;
|
||||
}
|
||||
|
||||
@mixin transform($transform) {
|
||||
-webkit-transform: $transform;
|
||||
-ms-transform: $transform;
|
||||
transform: $transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefilled mixins
|
||||
* Mixins with fixed values
|
||||
|
|
|
|||
|
|
@ -69,3 +69,39 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-profile-crop {
|
||||
.modal-dialog {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
p {
|
||||
display: table;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 400px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.cropper-bg {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.cropper-crop-box {
|
||||
box-sizing: content-box;
|
||||
border: 999px solid transparentize(#ccc, 0.5);
|
||||
@include transform(translate(-999px, -999px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.modal-profile-crop .modal-dialog {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ class ProfilesController < Profiles::ApplicationController
|
|||
|
||||
def user_params
|
||||
params.require(:user).permit(
|
||||
:avatar_crop_x,
|
||||
:avatar_crop_y,
|
||||
:avatar_crop_size,
|
||||
:avatar,
|
||||
:bio,
|
||||
:email,
|
||||
|
|
|
|||
|
|
@ -98,6 +98,11 @@ class User < ActiveRecord::Base
|
|||
# Virtual attribute for authenticating by either username or email
|
||||
attr_accessor :login
|
||||
|
||||
# Virtual attributes to define avatar cropping
|
||||
[:avatar_crop_x, :avatar_crop_y, :avatar_crop_size].each do |field|
|
||||
attr_accessor field
|
||||
end
|
||||
|
||||
#
|
||||
# Relations
|
||||
#
|
||||
|
|
@ -146,6 +151,11 @@ class User < ActiveRecord::Base
|
|||
# Validations
|
||||
#
|
||||
validates :name, presence: true
|
||||
|
||||
[:avatar_crop_x, :avatar_crop_y, :avatar_crop_size].each do |field|
|
||||
validates field, numericality: { only_integer: true }, allow_blank: true
|
||||
end
|
||||
|
||||
# Note that a 'uniqueness' and presence check is provided by devise :validatable for email. We do not need to
|
||||
# duplicate that here as the validation framework will have duplicate errors in the event of a failure.
|
||||
validates :email, presence: true, email: { strict_mode: true }
|
||||
|
|
|
|||
|
|
@ -2,11 +2,20 @@
|
|||
|
||||
class AvatarUploader < CarrierWave::Uploader::Base
|
||||
include UploaderHelper
|
||||
include CarrierWave::MiniMagick
|
||||
|
||||
storage :file
|
||||
|
||||
after :store, :reset_events_cache
|
||||
|
||||
process :cropper
|
||||
|
||||
def cropper
|
||||
manipulate! do |img|
|
||||
img.crop "#{model.avatar_crop_size}x#{model.avatar_crop_size}+#{model.avatar_crop_x}+#{model.avatar_crop_y}"
|
||||
end
|
||||
end
|
||||
|
||||
def store_dir
|
||||
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -88,8 +88,11 @@
|
|||
%i.fa.fa-paperclip
|
||||
%span Choose File ...
|
||||
|
||||
%span.file_name.js-avatar-filename File name...
|
||||
%span.file_name.js-avatar-filename{:'data-label' => 'File name...'} File name...
|
||||
= f.file_field :avatar, class: "js-user-avatar-input hidden"
|
||||
= f.hidden_field :avatar_crop_x
|
||||
= f.hidden_field :avatar_crop_y
|
||||
= f.hidden_field :avatar_crop_size
|
||||
.light The maximum file size allowed is 200KB.
|
||||
- if @user.avatar?
|
||||
%hr
|
||||
|
|
@ -99,3 +102,19 @@
|
|||
.form-actions
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
= link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
|
||||
|
||||
.modal.modal-profile-crop
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
.modal-header
|
||||
%button.close{:type => "button", :'data-dismiss' => "modal"}
|
||||
%span
|
||||
×
|
||||
%h4.modal-title
|
||||
Crop your new profile picture
|
||||
.modal-body
|
||||
%p
|
||||
%img.modal-profile-crop-image
|
||||
.modal-footer
|
||||
%button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
|
||||
Set new profile picture
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,379 @@
|
|||
/*!
|
||||
* Cropper v2.2.5
|
||||
* https://github.com/fengyuanchen/cropper
|
||||
*
|
||||
* Copyright (c) 2014-2016 Fengyuan Chen and contributors
|
||||
* Released under the MIT license
|
||||
*
|
||||
* Date: 2016-01-18T05:42:29.639Z
|
||||
*/
|
||||
.cropper-container {
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
|
||||
position: relative;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
direction: ltr !important;
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.cropper-container img {
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
min-width: 0 !important;
|
||||
max-width: none !important;
|
||||
height: 100%;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
|
||||
image-orientation: 0deg !important;
|
||||
}
|
||||
|
||||
.cropper-wrap-box,
|
||||
.cropper-canvas,
|
||||
.cropper-drag-box,
|
||||
.cropper-crop-box,
|
||||
.cropper-modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.cropper-wrap-box {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cropper-drag-box {
|
||||
opacity: 0;
|
||||
background-color: #fff;
|
||||
|
||||
filter: alpha(opacity=0);
|
||||
}
|
||||
|
||||
.cropper-modal {
|
||||
opacity: .5;
|
||||
background-color: #000;
|
||||
|
||||
filter: alpha(opacity=50);
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
outline: 1px solid #39f;
|
||||
outline-color: rgba(51, 153, 255, .75);
|
||||
}
|
||||
|
||||
.cropper-dashed {
|
||||
position: absolute;
|
||||
|
||||
display: block;
|
||||
|
||||
opacity: .5;
|
||||
border: 0 dashed #eee;
|
||||
|
||||
filter: alpha(opacity=50);
|
||||
}
|
||||
|
||||
.cropper-dashed.dashed-h {
|
||||
top: 33.33333%;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 33.33333%;
|
||||
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.cropper-dashed.dashed-v {
|
||||
top: 0;
|
||||
left: 33.33333%;
|
||||
|
||||
width: 33.33333%;
|
||||
height: 100%;
|
||||
|
||||
border-right-width: 1px;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
.cropper-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
display: block;
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
opacity: .75;
|
||||
|
||||
filter: alpha(opacity=75);
|
||||
}
|
||||
|
||||
.cropper-center:before,
|
||||
.cropper-center:after {
|
||||
position: absolute;
|
||||
|
||||
display: block;
|
||||
|
||||
content: ' ';
|
||||
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.cropper-center:before {
|
||||
top: 0;
|
||||
left: -3px;
|
||||
|
||||
width: 7px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.cropper-center:after {
|
||||
top: -3px;
|
||||
left: 0;
|
||||
|
||||
width: 1px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.cropper-face,
|
||||
.cropper-line,
|
||||
.cropper-point {
|
||||
position: absolute;
|
||||
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
opacity: .1;
|
||||
|
||||
filter: alpha(opacity=10);
|
||||
}
|
||||
|
||||
.cropper-face {
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.cropper-line {
|
||||
background-color: #39f;
|
||||
}
|
||||
|
||||
.cropper-line.line-e {
|
||||
top: 0;
|
||||
right: -3px;
|
||||
|
||||
width: 5px;
|
||||
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.cropper-line.line-n {
|
||||
top: -3px;
|
||||
left: 0;
|
||||
|
||||
height: 5px;
|
||||
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.cropper-line.line-w {
|
||||
top: 0;
|
||||
left: -3px;
|
||||
|
||||
width: 5px;
|
||||
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.cropper-line.line-s {
|
||||
bottom: -3px;
|
||||
left: 0;
|
||||
|
||||
height: 5px;
|
||||
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.cropper-point {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
opacity: .75;
|
||||
background-color: #39f;
|
||||
|
||||
filter: alpha(opacity=75);
|
||||
}
|
||||
|
||||
.cropper-point.point-e {
|
||||
top: 50%;
|
||||
right: -3px;
|
||||
|
||||
margin-top: -3px;
|
||||
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.cropper-point.point-n {
|
||||
top: -3px;
|
||||
left: 50%;
|
||||
|
||||
margin-left: -3px;
|
||||
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.cropper-point.point-w {
|
||||
top: 50%;
|
||||
left: -3px;
|
||||
|
||||
margin-top: -3px;
|
||||
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.cropper-point.point-s {
|
||||
bottom: -3px;
|
||||
left: 50%;
|
||||
|
||||
margin-left: -3px;
|
||||
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.cropper-point.point-ne {
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
|
||||
cursor: ne-resize;
|
||||
}
|
||||
|
||||
.cropper-point.point-nw {
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
|
||||
cursor: nw-resize;
|
||||
}
|
||||
|
||||
.cropper-point.point-sw {
|
||||
bottom: -3px;
|
||||
left: -3px;
|
||||
|
||||
cursor: sw-resize;
|
||||
}
|
||||
|
||||
.cropper-point.point-se {
|
||||
right: -3px;
|
||||
bottom: -3px;
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
cursor: se-resize;
|
||||
|
||||
opacity: 1;
|
||||
|
||||
filter: alpha(opacity=100);
|
||||
}
|
||||
|
||||
.cropper-point.point-se:before {
|
||||
position: absolute;
|
||||
right: -50%;
|
||||
bottom: -50%;
|
||||
|
||||
display: block;
|
||||
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
|
||||
content: ' ';
|
||||
|
||||
opacity: 0;
|
||||
background-color: #39f;
|
||||
|
||||
filter: alpha(opacity=0);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.cropper-point.point-se {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.cropper-point.point-se {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.cropper-point.point-se {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
opacity: .75;
|
||||
|
||||
filter: alpha(opacity=75);
|
||||
}
|
||||
}
|
||||
|
||||
.cropper-invisible {
|
||||
opacity: 0;
|
||||
|
||||
filter: alpha(opacity=0);
|
||||
}
|
||||
|
||||
.cropper-bg {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
|
||||
}
|
||||
|
||||
.cropper-hide {
|
||||
position: absolute;
|
||||
|
||||
display: block;
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.cropper-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cropper-move {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.cropper-crop {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.cropper-disabled .cropper-drag-box,
|
||||
.cropper-disabled .cropper-face,
|
||||
.cropper-disabled .cropper-line,
|
||||
.cropper-disabled .cropper-point {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
Loading…
Reference in New Issue