wip: add client side cropper to profile image field (#26251)
gitea/authentic/pipeline/head Build started...
Details
gitea/authentic/pipeline/head Build started...
Details
This commit is contained in:
parent
3988b6fb37
commit
b6bd4e2b5c
|
@ -50,6 +50,11 @@ class ProfileImageField(FileField):
|
|||
def image_size(self):
|
||||
return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE
|
||||
|
||||
def widget_attrs(self, widget):
|
||||
return {
|
||||
'data-image-size': self.image_size,
|
||||
}
|
||||
|
||||
def clean(self, data, initial=None):
|
||||
if data is FILE_INPUT_CONTRADICTION or data is False or data is None:
|
||||
return super(ProfileImageField, self).clean(data, initial=initial)
|
||||
|
|
|
@ -263,3 +263,12 @@ class ProfileImageInput(ClearableFileInput):
|
|||
attrs = kwargs.pop('attrs', {})
|
||||
attrs['accept'] = 'image/*'
|
||||
super(ProfileImageInput, self).__init__(*args, attrs=attrs, **kwargs)
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('css/croppie.css',),
|
||||
}
|
||||
js = (
|
||||
xstatic('jquery', 'jquery.min.js'),
|
||||
'js/croppie.js',
|
||||
)
|
||||
|
|
|
@ -0,0 +1,250 @@
|
|||
.croppie-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.croppie-container .cr-image {
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: 0 0;
|
||||
max-height: none;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.croppie-container .cr-boundary {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* margin: 0 auto; */
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.croppie-container .cr-viewport,
|
||||
.croppie-container .cr-resizer {
|
||||
position: absolute;
|
||||
border: 2px solid #fff;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
box-shadow: 0 0 2000px 2000px rgba(0, 0, 0, 0.5);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.croppie-container .cr-resizer {
|
||||
z-index: 2;
|
||||
box-shadow: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.croppie-container .cr-resizer-vertical,
|
||||
.croppie-container .cr-resizer-horisontal {
|
||||
position: absolute;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.croppie-container .cr-resizer-vertical::after,
|
||||
.croppie-container .cr-resizer-horisontal::after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
background: #fff;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.croppie-container .cr-resizer-vertical {
|
||||
bottom: -5px;
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.croppie-container .cr-resizer-vertical::after {
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.croppie-container .cr-resizer-horisontal {
|
||||
right: -5px;
|
||||
cursor: col-resize;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.croppie-container .cr-resizer-horisontal::after {
|
||||
top: 50%;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.croppie-container .cr-original-image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.croppie-container .cr-vp-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.croppie-container .cr-overlay {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
cursor: move;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.croppie-container .cr-slider-wrap {
|
||||
width: 75%;
|
||||
margin: 15px 0px;
|
||||
/* text-align: center; */
|
||||
}
|
||||
|
||||
.croppie-result {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.croppie-result img {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.croppie-container .cr-image,
|
||||
.croppie-container .cr-overlay,
|
||||
.croppie-container .cr-viewport {
|
||||
-webkit-transform: translateZ(0);
|
||||
-moz-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/*************************************/
|
||||
/***** STYLING RANGE INPUT ***********/
|
||||
/*************************************/
|
||||
/*http://brennaobrien.com/blog/2014/05/style-input-type-range-in-every-browser.html */
|
||||
/*************************************/
|
||||
|
||||
.cr-slider {
|
||||
-webkit-appearance: none;
|
||||
/*removes default webkit styles*/
|
||||
/*border: 1px solid white; *//*fix for FF unable to apply focus style bug */
|
||||
width: 300px;
|
||||
/*required for proper track sizing in FF*/
|
||||
max-width: 100%;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.cr-slider::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cr-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
background: #ddd;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.cr-slider:focus {
|
||||
outline: none;
|
||||
}
|
||||
/*
|
||||
.cr-slider:focus::-webkit-slider-runnable-track {
|
||||
background: #ccc;
|
||||
}
|
||||
*/
|
||||
|
||||
.cr-slider::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cr-slider::-moz-range-thumb {
|
||||
border: none;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
background: #ddd;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
/*hide the outline behind the border*/
|
||||
.cr-slider:-moz-focusring {
|
||||
outline: 1px solid white;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.cr-slider::-ms-track {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
background: transparent;
|
||||
/*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */
|
||||
border-color: transparent;/*leave room for the larger thumb to overflow with a transparent border */
|
||||
border-width: 6px 0;
|
||||
color: transparent;/*remove default tick marks*/
|
||||
}
|
||||
.cr-slider::-ms-fill-lower {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.cr-slider::-ms-fill-upper {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.cr-slider::-ms-thumb {
|
||||
border: none;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
background: #ddd;
|
||||
margin-top:1px;
|
||||
}
|
||||
.cr-slider:focus::-ms-fill-lower {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.cr-slider:focus::-ms-fill-upper {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
/*******************************************/
|
||||
|
||||
/***********************************/
|
||||
/* Rotation Tools */
|
||||
/***********************************/
|
||||
.cr-rotate-controls {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
.cr-rotate-controls button {
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
.cr-rotate-controls i:before {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-size: 22px;
|
||||
}
|
||||
.cr-rotate-l i:before {
|
||||
content: '↺';
|
||||
}
|
||||
.cr-rotate-r i:before {
|
||||
content: '↻';
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,92 @@
|
|||
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}"><img src="{{ widget.value.url }}"/></a>{% if not widget.required %}
|
||||
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}" />
|
||||
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br />
|
||||
{{ widget.input_text }}:{% endif %}
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />
|
||||
{% load i18n %}
|
||||
<div>
|
||||
<div id="{{ widget.attrs.id }}_preview"></div>
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />
|
||||
|
||||
<button type="button" id="{{ widget.attrs.id}}_load_image_btn">{% trans "Load image" %}</button>
|
||||
{% if widget.is_initial and not widget.required%}
|
||||
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}
|
||||
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}" />
|
||||
</label>
|
||||
{% endif %}
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
var default_name = "{{ widget.value.name }}";
|
||||
var base_id = "{{ widget.attrs.id }}";
|
||||
var initial_url = "{{ widget.value.url }}";
|
||||
//return a promise that resolves with a File instance
|
||||
function urltoFile(url, filename, mimeType){
|
||||
mimeType = mimeType || (url.match(/^data:([^;]+);/)||'')[1];
|
||||
return (fetch(url)
|
||||
.then(function(res){return res.arrayBuffer();})
|
||||
.then(function(buf){return new File([buf], filename, {type:mimeType});})
|
||||
);
|
||||
}
|
||||
// Hack to create FileList objects, https://gist.github.com/guest271314/7eac2c21911f5e40f48933ac78e518bd#file-setfilelist-html
|
||||
class _DataTransfer {
|
||||
constructor() {
|
||||
return new ClipboardEvent("").clipboardData || new DataTransfer();
|
||||
}
|
||||
}
|
||||
var $fileinput = $('#' + base_id);
|
||||
var $preview = $('#' + base_id + '_preview');
|
||||
var $checkbox = $('#{{ widget.checkbox_id }}');
|
||||
var image_size = $fileinput.data('image-size') || 180;
|
||||
$preview.croppie({
|
||||
viewport: {
|
||||
width: image_size,
|
||||
height: image_size,
|
||||
},
|
||||
boundary: {
|
||||
width: 300,
|
||||
height: 300,
|
||||
},
|
||||
zoom: 1.0,
|
||||
});
|
||||
if (! initial_url) {
|
||||
$preview.hide();
|
||||
} else {
|
||||
$preview.croppie('bind', {
|
||||
url: initial_url,
|
||||
zoom: 1.0,
|
||||
});
|
||||
}
|
||||
$fileinput.hide();
|
||||
$('#' + base_id + '_load_image_btn').on('click', function () {
|
||||
$fileinput.click();
|
||||
});
|
||||
$fileinput.on('change', function () {
|
||||
$checkbox.prop('checked', false);
|
||||
var file = $fileinput[0].files[0];
|
||||
var reader = new FileReader();
|
||||
reader.addEventListener("load", function () {
|
||||
$preview.croppie('bind', {
|
||||
url: reader.result,
|
||||
});
|
||||
$preview.show();
|
||||
});
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
$preview.on('update.croppie', function () {
|
||||
if ($checkbox.prop('checked')) {
|
||||
return;
|
||||
}
|
||||
$preview.croppie('result', {type: "blob", format: "jpeg"}).then(function (blob) {
|
||||
var data_transfer = new _DataTransfer();
|
||||
var file = new File([blob], default_name || 'avatar.jpg', {type: 'image/jpeg'});
|
||||
data_transfer.items.add(file);
|
||||
|
||||
$fileinput[0].files = data_transfer.files;
|
||||
console.log('update fileinput with file', data_transfer.files[0], 'of length', data_transfer.files[0].size);
|
||||
});
|
||||
});
|
||||
$checkbox.on('change', function (ev) {
|
||||
if ($checkbox.prop('checked')) {
|
||||
$fileinput.val('');
|
||||
} else {
|
||||
$preview.trigger('update.croppie');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue