mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-21 10:36:04 +10:00
Merge branch 'issue_7959' into 'master'
Crop avatars on the client before upload <img src="/uploads/535d69956cc5904870ae5d95132a35af/Screen_Shot_2016-03-17_at_9.55.56_AM.png" width="700"> closes #7959 closes #600 See merge request !3270
This commit is contained in:
@@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date.
|
||||
v 8.7.0 (unreleased)
|
||||
- Preserve time notes/comments have been updated at when moving issue
|
||||
- Make HTTP(s) label consistent on clone bar (Stan Hu)
|
||||
- Fix avatar stretching by providing a cropping feature
|
||||
|
||||
v 8.6.1
|
||||
- Add option to reload the schema before restoring a database backup. !2807
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#= require jquery.nicescroll
|
||||
#= require_tree .
|
||||
#= require fuzzaldrin-plus
|
||||
#= require cropper
|
||||
|
||||
window.slugify = (text) ->
|
||||
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
class GitLabCrop
|
||||
# Matches everything but the file name
|
||||
FILENAMEREGEX = /^.*[\\\/]/
|
||||
|
||||
constructor: (input, opts = {}) ->
|
||||
@fileInput = $(input)
|
||||
|
||||
# We should rename to avoid spec to fail
|
||||
# Form will submit the proper input filed with a file using FormData
|
||||
@fileInput
|
||||
.attr('name', "#{@fileInput.attr('name')}-trigger")
|
||||
.attr('id', "#{@fileInput.attr('id')}-trigger")
|
||||
|
||||
# Set defaults
|
||||
{
|
||||
@exportWidth = 200
|
||||
@exportHeight = 200
|
||||
@cropBoxWidth = 200
|
||||
@cropBoxHeight = 200
|
||||
@form = @fileInput.parents('form')
|
||||
|
||||
# Required params
|
||||
@filename
|
||||
@previewImage
|
||||
@modalCrop
|
||||
@pickImageEl
|
||||
@uploadImageBtn
|
||||
@modalCropImg
|
||||
} = opts
|
||||
|
||||
# Ensure needed elements are jquery objects
|
||||
# If selector is provided we will convert them to a jQuery Object
|
||||
@filename = @getElement(@filename)
|
||||
@previewImage = @getElement(@previewImage)
|
||||
@pickImageEl = @getElement(@pickImageEl)
|
||||
|
||||
# Modal elements usually are outside the @form element
|
||||
@modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop
|
||||
@uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn
|
||||
@modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg
|
||||
|
||||
@cropActionsBtn = @modalCrop.find('[data-method]')
|
||||
|
||||
@bindEvents()
|
||||
|
||||
getElement: (selector) ->
|
||||
$(selector, @form)
|
||||
|
||||
bindEvents: ->
|
||||
_this = @
|
||||
@fileInput.on 'change', (e) ->
|
||||
_this.onFileInputChange(e, @)
|
||||
|
||||
@pickImageEl.on 'click', @onPickImageClick
|
||||
@modalCrop.on 'shown.bs.modal', @onModalShow
|
||||
@modalCrop.on 'hidden.bs.modal', @onModalHide
|
||||
@uploadImageBtn.on 'click', @onUploadImageBtnClick
|
||||
@cropActionsBtn.on 'click', (e) ->
|
||||
btn = @
|
||||
_this.onActionBtnClick(btn)
|
||||
@croppedImageBlob = null
|
||||
|
||||
onPickImageClick: =>
|
||||
@fileInput.trigger('click')
|
||||
|
||||
onModalShow: =>
|
||||
_this = @
|
||||
@modalCropImg.cropper(
|
||||
viewMode: 1
|
||||
center: false
|
||||
aspectRatio: 1
|
||||
modal: true
|
||||
scalable: false
|
||||
rotatable: false
|
||||
zoomable: true
|
||||
dragMode: 'move'
|
||||
guides: false
|
||||
zoomOnTouch: false
|
||||
zoomOnWheel: false
|
||||
cropBoxMovable: false
|
||||
cropBoxResizable: false
|
||||
toggleDragModeOnDblclick: false
|
||||
built: ->
|
||||
$image = $(@)
|
||||
container = $image.cropper 'getContainerData'
|
||||
cropBoxWidth = _this.cropBoxWidth;
|
||||
cropBoxHeight = _this.cropBoxHeight;
|
||||
|
||||
$image.cropper('setCropBoxData',
|
||||
width: cropBoxWidth,
|
||||
height: cropBoxHeight,
|
||||
left: (container.width - cropBoxWidth) / 2,
|
||||
top: (container.height - cropBoxHeight) / 2
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
onModalHide: =>
|
||||
@modalCropImg
|
||||
.attr('src', '') # Remove attached image
|
||||
.cropper('destroy') # Destroy cropper instance
|
||||
|
||||
onUploadImageBtnClick: (e) =>
|
||||
e.preventDefault()
|
||||
@setBlob()
|
||||
@setPreview()
|
||||
@modalCrop.modal('hide')
|
||||
@fileInput.val('')
|
||||
|
||||
onActionBtnClick: (btn) ->
|
||||
data = $(btn).data()
|
||||
|
||||
if @modalCropImg.data('cropper') && data.method
|
||||
result = @modalCropImg.cropper data.method, data.option
|
||||
|
||||
onFileInputChange: (e, input) ->
|
||||
@readFile(input)
|
||||
|
||||
readFile: (input) ->
|
||||
_this = @
|
||||
reader = new FileReader
|
||||
reader.onload = ->
|
||||
_this.modalCropImg.attr('src', reader.result)
|
||||
_this.modalCrop.modal('show')
|
||||
|
||||
reader.readAsDataURL(input.files[0])
|
||||
|
||||
dataURLtoBlob: (dataURL) ->
|
||||
binary = atob(dataURL.split(',')[1])
|
||||
array = []
|
||||
for v, k in binary
|
||||
array.push(binary.charCodeAt(k))
|
||||
new Blob([new Uint8Array(array)], type: 'image/png')
|
||||
|
||||
setPreview: ->
|
||||
@previewImage.attr('src', @dataURL)
|
||||
filename = @fileInput.val().replace(FILENAMEREGEX, '')
|
||||
@filename.text(filename)
|
||||
|
||||
setBlob: ->
|
||||
@dataURL = @modalCropImg.cropper('getCroppedCanvas',
|
||||
width: 200
|
||||
height: 200
|
||||
).toDataURL('image/png')
|
||||
@croppedImageBlob = @dataURLtoBlob(@dataURL)
|
||||
|
||||
getBlob: ->
|
||||
@croppedImageBlob
|
||||
|
||||
$.fn.glCrop = (opts) ->
|
||||
return @.each ->
|
||||
$(@).data('glcrop', new GitLabCrop(@, opts))
|
||||
@@ -1,5 +1,9 @@
|
||||
class @Profile
|
||||
constructor: ->
|
||||
constructor: (opts = {}) ->
|
||||
{
|
||||
@form = $('.edit-user')
|
||||
} = opts
|
||||
|
||||
# Automatically submit the Preferences form when any of its radio buttons change
|
||||
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
|
||||
$(this).parents('form').submit()
|
||||
@@ -17,14 +21,46 @@ 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()
|
||||
@bindEvents()
|
||||
|
||||
$('.js-user-avatar-input').bind "change", ->
|
||||
form = $(this).closest("form")
|
||||
filename = $(this).val().replace(/^.*[\\\/]/, '')
|
||||
form.find(".js-avatar-filename").text(filename)
|
||||
cropOpts =
|
||||
filename: '.js-avatar-filename'
|
||||
previewImage: '.avatar-image .avatar'
|
||||
modalCrop: '.modal-profile-crop'
|
||||
pickImageEl: '.js-choose-user-avatar-button'
|
||||
uploadImageBtn: '.js-upload-user-avatar'
|
||||
modalCropImg: '.modal-profile-crop-image'
|
||||
|
||||
@avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop'
|
||||
|
||||
bindEvents: ->
|
||||
@form.on 'submit', @onSubmitForm
|
||||
|
||||
onSubmitForm: (e) =>
|
||||
e.preventDefault()
|
||||
@saveForm()
|
||||
|
||||
saveForm: ->
|
||||
self = @
|
||||
|
||||
formData = new FormData(@form[0])
|
||||
formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png')
|
||||
|
||||
$.ajax
|
||||
url: @form.attr('action')
|
||||
type: @form.attr('method')
|
||||
data: formData
|
||||
dataType: "json"
|
||||
processData: false
|
||||
contentType: false
|
||||
success: (response) ->
|
||||
new Flash(response.message, 'notice')
|
||||
error: (jqXHR) ->
|
||||
new Flash(jqXHR.responseJSON.message, 'alert')
|
||||
complete: ->
|
||||
window.scrollTo 0, 0
|
||||
# Enable submit button after requests ends
|
||||
self.form.find(':input[disabled]').enable()
|
||||
|
||||
$ ->
|
||||
# Extract the SSH Key title from its comment
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*= require_self
|
||||
*= require dropzone/basic
|
||||
*= require cal-heatmap
|
||||
*= require cropper.css
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@@ -197,3 +197,24 @@
|
||||
width: 105px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-profile-crop {
|
||||
.modal-dialog {
|
||||
width: 380px;
|
||||
|
||||
@media (max-width: $screen-sm-min) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.profile-crop-image-container {
|
||||
height: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.crop-controls {
|
||||
padding: 10px 0 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,16 @@ class ProfilesController < Profiles::ApplicationController
|
||||
def update
|
||||
user_params.except!(:email) if @user.ldap_user?
|
||||
|
||||
if @user.update_attributes(user_params)
|
||||
flash[:notice] = "Profile was successfully updated"
|
||||
else
|
||||
messages = @user.errors.full_messages.uniq.join('. ')
|
||||
flash[:alert] = "Failed to update profile. #{messages}"
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default(default: { action: 'show' }) }
|
||||
if @user.update_attributes(user_params)
|
||||
message = "Profile was successfully updated"
|
||||
format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
|
||||
format.json { render json: { message: message } }
|
||||
else
|
||||
message = @user.errors.full_messages.uniq.join('. ')
|
||||
format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) }
|
||||
format.json { render json: { message: message }, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
%a.btn.js-choose-user-avatar-button
|
||||
Browse file...
|
||||
%span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
|
||||
= f.file_field :avatar, class: "js-user-avatar-input hidden"
|
||||
= f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*"
|
||||
.help-block
|
||||
The maximum file size allowed is 200KB.
|
||||
- if @user.avatar?
|
||||
@@ -94,3 +94,25 @@
|
||||
.prepend-top-default.append-bottom-default
|
||||
= f.submit 'Update profile settings', 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
|
||||
Position and size your new avatar
|
||||
.modal-body
|
||||
.profile-crop-image-container
|
||||
%img.modal-profile-crop-image
|
||||
.crop-controls
|
||||
.btn-group
|
||||
%button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } }
|
||||
%span.fa.fa-search-plus
|
||||
%button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } }
|
||||
%span.fa.fa-search-minus
|
||||
.modal-footer
|
||||
%button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
|
||||
Set new profile picture
|
||||
|
||||
Vendored
+2993
File diff suppressed because it is too large
Load Diff
+379
@@ -0,0 +1,379 @@
|
||||
/*!
|
||||
* Cropper v2.3.0
|
||||
* https://github.com/fengyuanchen/cropper
|
||||
*
|
||||
* Copyright (c) 2014-2016 Fengyuan Chen and contributors
|
||||
* Released under the MIT license
|
||||
*
|
||||
* Date: 2016-02-22T02:13:13.332Z
|
||||
*/
|
||||
.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;
|
||||
}
|
||||
Reference in New Issue
Block a user