[MERGE] web_editor: add the image cropping feature
Thanks to this merge, users are now able to crop their images using the editor. This is done by clicking on a content image and then the crop button appearing in the editor bar. Cropping an image will only crop the image at the location it was clicked by creating a new image for that location. Cropped images do not appear in the media dialog. task-43706 Closes https://github.com/odoo/odoo/pull/22473
This commit is contained in:
@@ -245,6 +245,23 @@ class Web_Editor(http.Controller):
|
||||
attachments_to_remove.unlink()
|
||||
return removal_blocked_by
|
||||
|
||||
## This route is used from CropImageDialog to get image info.
|
||||
## It is used to display the original image when we crop a previously
|
||||
## cropped image
|
||||
@http.route('/web_editor/get_image_info', type='json', auth='user')
|
||||
def get_image_info(self, image_id=None, xml_id=None):
|
||||
if xml_id:
|
||||
record = request.env.ref(xml_id)
|
||||
elif image_id:
|
||||
record = request.env['ir.attachment'].browse(image_id)
|
||||
result = {
|
||||
'id': record.id,
|
||||
'mimetype': record.mimetype,
|
||||
}
|
||||
if image_id and record.url:
|
||||
result['src'] = record.url
|
||||
return result
|
||||
|
||||
## The get_assets_editor_resources route is in charge of transmitting the resources the assets
|
||||
## editor needs to work.
|
||||
## @param key - the xml_id or id of the view the resources are related to
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/*!
|
||||
* Cropper v3.1.4
|
||||
* https://github.com/fengyuanchen/cropper
|
||||
*
|
||||
* Copyright (c) 2014-2018 Chen Fengyuan
|
||||
* Released under the MIT license
|
||||
*
|
||||
* Date: 2018-01-13T09:37:21.486Z
|
||||
*/
|
||||
|
||||
.cropper-container {
|
||||
direction: ltr;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cropper-container img {/*Avoid margin top issue (Occur only when margin-top <= -height)
|
||||
*/
|
||||
display: block;
|
||||
height: 100%;
|
||||
image-orientation: 0deg;
|
||||
max-height: none !important;
|
||||
max-width: none !important;
|
||||
min-height: 0 !important;
|
||||
min-width: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cropper-wrap-box,
|
||||
.cropper-canvas,
|
||||
.cropper-drag-box,
|
||||
.cropper-crop-box,
|
||||
.cropper-modal {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.cropper-wrap-box,
|
||||
.cropper-canvas {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cropper-drag-box {
|
||||
background-color: #fff;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cropper-modal {
|
||||
background-color: #000;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
display: block;
|
||||
height: 100%;
|
||||
outline-color: rgba(51, 153, 255, 0.75);
|
||||
outline: 1px solid #39f;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cropper-dashed {
|
||||
border: 0 dashed #eee;
|
||||
display: block;
|
||||
opacity: .5;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.cropper-dashed.dashed-h {
|
||||
border-bottom-width: 1px;
|
||||
border-top-width: 1px;
|
||||
height: 33.33333%;
|
||||
left: 0;
|
||||
top: 33.33333%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cropper-dashed.dashed-v {
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
height: 100%;
|
||||
left: 33.33333%;
|
||||
top: 0;
|
||||
width: 33.33333%;
|
||||
}
|
||||
|
||||
.cropper-center {
|
||||
display: block;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
opacity: .75;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.cropper-center:before,
|
||||
.cropper-center:after {
|
||||
background-color: #eee;
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.cropper-center:before {
|
||||
height: 1px;
|
||||
left: -3px;
|
||||
top: 0;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
.cropper-center:after {
|
||||
height: 7px;
|
||||
left: 0;
|
||||
top: -3px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.cropper-face,
|
||||
.cropper-line,
|
||||
.cropper-point {
|
||||
display: block;
|
||||
height: 100%;
|
||||
opacity: .1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cropper-face {
|
||||
background-color: #fff;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.cropper-line {
|
||||
background-color: #39f;
|
||||
}
|
||||
|
||||
.cropper-line.line-e {
|
||||
cursor: ew-resize;
|
||||
right: -3px;
|
||||
top: 0;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.cropper-line.line-n {
|
||||
cursor: ns-resize;
|
||||
height: 5px;
|
||||
left: 0;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.cropper-line.line-w {
|
||||
cursor: ew-resize;
|
||||
left: -3px;
|
||||
top: 0;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.cropper-line.line-s {
|
||||
bottom: -3px;
|
||||
cursor: ns-resize;
|
||||
height: 5px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.cropper-point {
|
||||
background-color: #39f;
|
||||
height: 5px;
|
||||
opacity: .75;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.cropper-point.point-e {
|
||||
cursor: ew-resize;
|
||||
margin-top: -3px;
|
||||
right: -3px;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.cropper-point.point-n {
|
||||
cursor: ns-resize;
|
||||
left: 50%;
|
||||
margin-left: -3px;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-w {
|
||||
cursor: ew-resize;
|
||||
left: -3px;
|
||||
margin-top: -3px;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.cropper-point.point-s {
|
||||
bottom: -3px;
|
||||
cursor: s-resize;
|
||||
left: 50%;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-ne {
|
||||
cursor: nesw-resize;
|
||||
right: -3px;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-nw {
|
||||
cursor: nwse-resize;
|
||||
left: -3px;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-sw {
|
||||
bottom: -3px;
|
||||
cursor: nesw-resize;
|
||||
left: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-se {
|
||||
bottom: -3px;
|
||||
cursor: nwse-resize;
|
||||
height: 20px;
|
||||
opacity: 1;
|
||||
right: -3px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.cropper-point.point-se {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.cropper-point.point-se {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.cropper-point.point-se {
|
||||
height: 5px;
|
||||
opacity: .75;
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.cropper-point.point-se:before {
|
||||
background-color: #39f;
|
||||
bottom: -50%;
|
||||
content: ' ';
|
||||
display: block;
|
||||
height: 200%;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
}
|
||||
|
||||
.cropper-invisible {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cropper-bg {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
|
||||
}
|
||||
|
||||
.cropper-hide {
|
||||
display: block;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
width: 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;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -157,6 +157,11 @@ renderer.tplPopovers = function (lang, options) {
|
||||
event: 'delete'
|
||||
})).appendTo($imageprop);
|
||||
|
||||
$(tplIconButton('fa fa-crop', {
|
||||
title: _t('Crop Image'),
|
||||
event: 'cropImage',
|
||||
})).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="img-thumbnail"]'));
|
||||
|
||||
$imagePopover.find('.popover-content').append($airPopover.find(".note-history").clone());
|
||||
|
||||
$imagePopover.find('[data-event="showImageDialog"]').before($airPopover.find('[data-event="showLinkDialog"]').clone());
|
||||
@@ -410,6 +415,14 @@ $.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) {
|
||||
media: $selection.data('target'),
|
||||
});
|
||||
};
|
||||
$.summernote.pluginEvents.cropImage = function (event, editor, layoutInfo, sorted) {
|
||||
var $editable = layoutInfo.editable();
|
||||
var $selection = layoutInfo.handle().find('.note-control-selection');
|
||||
core.bus.trigger('crop_image_dialog_demand', {
|
||||
$editable: $editable,
|
||||
media: $selection.data('target'),
|
||||
});
|
||||
};
|
||||
|
||||
// Utils
|
||||
var fn_is_void = dom.isVoid || function () {};
|
||||
@@ -1024,6 +1037,7 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
|
||||
this.setParent(parent);
|
||||
|
||||
core.bus.on('alt_dialog_demand', this, this._onAltDialogDemand);
|
||||
core.bus.on('crop_image_dialog_demand', this, this._onCropImageDialogDemand);
|
||||
core.bus.on('link_dialog_demand', this, this._onLinkDialogDemand);
|
||||
core.bus.on('media_dialog_demand', this, this._onMediaDialogDemand);
|
||||
},
|
||||
@@ -1034,6 +1048,7 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
|
||||
mixins.EventDispatcherMixin.destroy.call(this);
|
||||
|
||||
core.bus.off('alt_dialog_demand', this, this._onAltDialogDemand);
|
||||
core.bus.off('crop_image_dialog_demand', this, this._onCropImageDialogDemand);
|
||||
core.bus.off('link_dialog_demand', this, this._onLinkDialogDemand);
|
||||
core.bus.off('media_dialog_demand', this, this._onMediaDialogDemand);
|
||||
},
|
||||
@@ -1066,6 +1081,34 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
|
||||
}
|
||||
altDialog.open();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a demand to open a crop dialog is received on the bus.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
*/
|
||||
_onCropImageDialogDemand: function (data) {
|
||||
if (data.__alreadyDone) {
|
||||
return;
|
||||
}
|
||||
data.__alreadyDone = true;
|
||||
var cropImageDialog = new weWidgets.CropImageDialog(this,
|
||||
_.extend({
|
||||
res_model: data.$editable.data('oe-model'),
|
||||
res_id: data.$editable.data('oe-id'),
|
||||
}, data.options || {}),
|
||||
data.$editable,
|
||||
data.media
|
||||
);
|
||||
if (data.onSave) {
|
||||
cropImageDialog.on('save', this, data.onSave);
|
||||
}
|
||||
if (data.onCancel) {
|
||||
cropImageDialog.on('cancel', this, data.onCancel);
|
||||
}
|
||||
cropImageDialog.open();
|
||||
},
|
||||
/**
|
||||
* Called when a demand to open a link dialog is received on the bus.
|
||||
*
|
||||
|
||||
@@ -640,6 +640,17 @@ var SnippetsMenu = Widget.extend({
|
||||
this.$editable.find('[contentEditable]')
|
||||
.removeAttr('contentEditable')
|
||||
.removeProp('contentEditable');
|
||||
|
||||
// Removing query_string used to invalidate cache during image crop
|
||||
_.each(this.$editable.find('.o_cropped_img_to_clean'), function (croppedImg) {
|
||||
var $croppedImg = $(croppedImg);
|
||||
var s = $croppedImg.attr('src').split('?');
|
||||
var src = s[0];
|
||||
var urlParams = s[1] || '';
|
||||
urlParams = $.param(_.omit($.deparam(urlParams), 'unique'));
|
||||
$croppedImg.attr('src', src + (urlParams ? ('?' + urlParams) : ''));
|
||||
$croppedImg.removeClass('o_cropped_img_to_clean');
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
odoo.define('web_editor.widget', function (require) {
|
||||
'use strict';
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var base = require('web_editor.base');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var Widget = require('web.Widget');
|
||||
var utils = require('web.utils');
|
||||
var weContext = require("web_editor.context");
|
||||
|
||||
var QWeb = core.qweb;
|
||||
@@ -337,6 +337,14 @@ var ImageWidget = MediaWidget.extend({
|
||||
self.options.onUpload([img]);
|
||||
}
|
||||
|
||||
// Remove crop related attributes
|
||||
if (self.$media.attr('data-aspect-ratio')) {
|
||||
var attrs = ['aspect-ratio', 'x', 'y', 'width', 'height', 'rotate', 'scale-x', 'scale-y'];
|
||||
_.each(attrs, function (attr) {
|
||||
self.$media.removeData(attr);
|
||||
self.$media.removeAttr('data-' + attr);
|
||||
});
|
||||
}
|
||||
return self.media;
|
||||
});
|
||||
},
|
||||
@@ -1541,10 +1549,275 @@ var LinkDialog = Dialog.extend({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* CropImageDialog widget. Let users crop an image.
|
||||
*/
|
||||
var CropImageDialog = Dialog.extend({
|
||||
template: 'web_editor.dialog.crop_image',
|
||||
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
|
||||
['/web_editor/static/src/xml/editor.xml']
|
||||
),
|
||||
jsLibs: [
|
||||
'/web_editor/static/lib/cropper/js/cropper.js',
|
||||
],
|
||||
cssLibs: [
|
||||
'/web_editor/static/lib/cropper/css/cropper.css',
|
||||
],
|
||||
events : _.extend({}, Dialog.prototype.events, {
|
||||
'click .o_crop_options [data-event]': '_onCropOptionClick',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, options, $editable, media) {
|
||||
this.media = media;
|
||||
this.$media = $(this.media);
|
||||
var srcSplit = this.$media.attr('src').split('?');
|
||||
var src = srcSplit[0];
|
||||
this.imageData = {
|
||||
initialSrc: src,
|
||||
src: src, // the original src for cropped DB images will be fetched later
|
||||
srcParams: $.deparam(srcSplit[1] || ''),
|
||||
mimetype: _.str.endsWith(src, '.png') ? 'image/png' : 'image/jpeg', // the mimetype for DB images will be fetched later
|
||||
aspectRatio: this.$media.data('aspectRatio') || 0,
|
||||
isExternalImage: src[0] !== '/' && src.indexOf(window.location.host) < 0,
|
||||
};
|
||||
this.aspectRatioList = [
|
||||
[_t("Free"), 0],
|
||||
['16:9', 16 / 9],
|
||||
['4:3', 4 / 3],
|
||||
['1:1', 1],
|
||||
['2:3', 2 / 3],
|
||||
];
|
||||
this.options = _.extend({
|
||||
title: _t("Crop Image"),
|
||||
buttons: this.imageData.isExternalImage ? [{
|
||||
text: _t("Close"),
|
||||
close: true,
|
||||
}] : [{
|
||||
text: _t("Save"),
|
||||
classes: 'btn-primary',
|
||||
click: this.save,
|
||||
}, {
|
||||
text: _t("Discard"),
|
||||
close: true,
|
||||
}],
|
||||
}, options || {});
|
||||
this._super(parent, this.options);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
var def = this._super.apply(this, arguments);
|
||||
if (this.imageData.isExternalImage) {
|
||||
return def;
|
||||
}
|
||||
|
||||
var defs = [def, ajax.loadLibs(this)];
|
||||
var params = {};
|
||||
var isDBImage = false;
|
||||
var matchImageID = this.imageData.src.match(/^\/web\/image\/(\d+)/);
|
||||
if (matchImageID) {
|
||||
params['image_id'] = parseInt(matchImageID[1]);
|
||||
isDBImage = true;
|
||||
} else {
|
||||
var matchXmlID = this.imageData.src.match(/^\/web\/image\/([^/?]+)/);
|
||||
if (matchXmlID) {
|
||||
params['xml_id'] = matchXmlID[1];
|
||||
isDBImage = true;
|
||||
}
|
||||
}
|
||||
if (isDBImage) {
|
||||
defs.push(this._rpc({
|
||||
route: '/web_editor/get_image_info',
|
||||
params: params,
|
||||
}).then(function (res) {
|
||||
_.extend(self.imageData, res);
|
||||
}));
|
||||
}
|
||||
return $.when.apply($, defs);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$cropperImage = this.$('.o_cropper_image');
|
||||
if (this.$cropperImage.length) {
|
||||
this.$cropperImage.cropper({
|
||||
viewMode: 1,
|
||||
autoCropArea: 1,
|
||||
data: _.pick(this.$media.data(), 'aspectRatio', 'x', 'y', 'width', 'height', 'rotate', 'scaleX', 'scaleY')
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
if (this.$cropperImage.length) {
|
||||
this.$cropperImage.cropper('destroy');
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Stores the cropped image in database.
|
||||
*
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
save: function () {
|
||||
var _super = this._super.bind(this);
|
||||
var def;
|
||||
if (this.imageData.initialSrc === this.imageData.src) {
|
||||
// When this is a new image being cropped
|
||||
def = this._createImage();
|
||||
} else {
|
||||
// When this is a previously cropped image
|
||||
def = this._updateImage();
|
||||
}
|
||||
return def.then(_super);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets the base64 data from the cropper canvas.
|
||||
*
|
||||
* @private
|
||||
* @returns {string} base64 image data
|
||||
*/
|
||||
_getImageBase64: function () {
|
||||
var cropperData = this.$cropperImage.cropper('getData');
|
||||
var canvas = this.$cropperImage.cropper('getCroppedCanvas', {
|
||||
width: cropperData.width,
|
||||
height: cropperData.height,
|
||||
});
|
||||
return canvas.toDataURL(this.imageData.mimetype).split(',')[1];
|
||||
},
|
||||
/**
|
||||
* Creates a new attachment for the cropped image data and updates the image
|
||||
* with the correct URL.
|
||||
*
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_createImage: function () {
|
||||
var self = this;
|
||||
var name = this.imageData.src + '.crop';
|
||||
return this._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'create',
|
||||
args: [{
|
||||
res_model: this.options.res_model,
|
||||
res_id: this.options.res_id,
|
||||
name: name,
|
||||
datas_fname: name,
|
||||
datas: this._getImageBase64(),
|
||||
mimetype: this.imageData.mimetype,
|
||||
url: this.imageData.src, // To save the original image that was cropped
|
||||
}],
|
||||
}).then(function (attachmentID) {
|
||||
return self._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'generate_access_token',
|
||||
args: [[attachmentID]],
|
||||
}).then(function (access_token) {
|
||||
var params = _.omit(self.imageData.srcParams, 'unique');
|
||||
params.access_token = access_token[0];
|
||||
self.$media.attr('src', '/web/image/' + attachmentID + '?' + $.param(params));
|
||||
self._setCropBoxData();
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Writes the cropped image attachment with the new data and forces a
|
||||
* refresh of the image.
|
||||
*
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_updateImage: function () {
|
||||
var self = this;
|
||||
return this._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'write',
|
||||
args: [[this.imageData.id], {datas: this._getImageBase64()}],
|
||||
}).then(function () {
|
||||
self.$media.addClass('o_cropped_img_to_clean');
|
||||
var params = _.clone(self.imageData.srcParams);
|
||||
params.unique = new Date().getTime();
|
||||
self.$media.attr('src', '/web/image/' + self.imageData.id + '?' + $.param(params));
|
||||
self._setCropBoxData();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Saves current crop configuration data as image attributes for future
|
||||
* edition.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_setCropBoxData: function () {
|
||||
var self = this;
|
||||
var cropBoxData = _.extend({
|
||||
aspectRatio: this.imageData.aspectRatio, // Library's getData does not export aspect ratio
|
||||
}, this.$cropperImage.cropper('getData'));
|
||||
_.each(cropBoxData, function (value, key) {
|
||||
key = _.str.dasherize(key);
|
||||
self.$media.attr('data-' + key, value);
|
||||
self.$media.data(key, value);
|
||||
});
|
||||
this.$media.trigger('content_changed');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when a crop option is clicked -> change the crop area accordingly.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onCropOptionClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
var $option = $(ev.currentTarget);
|
||||
var opt = $option.data('event');
|
||||
var value = $option.data('value');
|
||||
switch (opt) {
|
||||
case 'ratio':
|
||||
$option.siblings().removeClass('active');
|
||||
$option.addClass('active');
|
||||
this.$cropperImage.cropper('reset');
|
||||
this.imageData.aspectRatio = value;
|
||||
this.$cropperImage.cropper('setAspectRatio', this.imageData.aspectRatio);
|
||||
break;
|
||||
case 'zoom':
|
||||
case 'rotate':
|
||||
case 'reset':
|
||||
this.$cropperImage.cropper(opt, value);
|
||||
break;
|
||||
case 'flip':
|
||||
var direction = value === 'horizontal' ? 'x' : 'y';
|
||||
var scaleAngle = -$option.data(direction);
|
||||
$option.data(direction, scaleAngle);
|
||||
this.$cropperImage.cropper('scale' + direction.toUpperCase(), scaleAngle);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
Dialog: Dialog,
|
||||
AltDialog: AltDialog,
|
||||
MediaDialog: MediaDialog,
|
||||
LinkDialog: LinkDialog,
|
||||
CropImageDialog: CropImageDialog,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1664,4 +1664,25 @@ body .modal {
|
||||
border-left: 1px solid $gray-lighter;
|
||||
}
|
||||
}
|
||||
// Crop Dialog
|
||||
.o_crop_image_dialog .o_crop_area {
|
||||
height: 350px;
|
||||
.cropper-point {
|
||||
background-color: white;
|
||||
@include o-transition(all, 400ms, $o-we-md-ease);
|
||||
@include o-transform(scale(1.5, 1.5));
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 1px 1px rgba(23, 23, 23, 0.5);
|
||||
opacity: 1;
|
||||
&:hover {
|
||||
@include o-transform(scale(2.5, 2.5));
|
||||
}
|
||||
}
|
||||
.cropper-line {
|
||||
background: black;
|
||||
}
|
||||
.cropper-view-box {
|
||||
outline: 2px solid white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +300,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Crop Image Dialog (allows to crop image on the page) -->
|
||||
<div t-name="web_editor.dialog.crop_image" class="o_crop_image_dialog">
|
||||
<div t-if="widget.imageData.isExternalImage" class="alert alert-warning text-center">
|
||||
<i class="fa fa-exclamation-triangle fa-2x"/>
|
||||
<h4>This image is an external image</h4>
|
||||
<p>
|
||||
This type of image is not supported for cropping.<br/>
|
||||
If you want to crop it, please first download it from the original source and upload it in Odoo.
|
||||
</p>
|
||||
</div>
|
||||
<t t-else="">
|
||||
<div class="o_crop_area">
|
||||
<img class="img img-responsive o_cropper_image" t-att-src="widget.imageData.src" />
|
||||
</div>
|
||||
<div class="o_crop_options text-center mt16">
|
||||
<div class="btn-group" title="Aspect Ratio" data-toggle="buttons">
|
||||
<t t-foreach="widget.aspectRatioList" t-as="ratio">
|
||||
<t t-set="is_active" t-value="ratio[1] === widget.imageData.aspectRatio"/>
|
||||
<a href="#" t-attf-class="btn btn-default#{is_active and ' active' or ''}" data-event="ratio" t-att-data-value="ratio[1]">
|
||||
<t t-esc="ratio[0]"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="#" class="btn btn-default" title="Zoom In" data-event="zoom" data-value="0.1"><i class="fa fa-search-plus"/></a>
|
||||
<a href="#" class="btn btn-default" title="Zoom Out" data-event="zoom" data-value="-0.1"><i class="fa fa-search-minus"/></a>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="#" class="btn btn-default" title="Rotate Left" data-event="rotate" data-value="-45"><i class="fa fa-rotate-left"/></a>
|
||||
<a href="#" class="btn btn-default" title="Rotate Right" data-event="rotate" data-value="45"><i class="fa fa-rotate-right"/></a>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="#" class="btn btn-default" title="Flip Horizontal" data-event="flip" data-value="horizontal" data-x="1"><i class="fa fa-arrows-h"/></a>
|
||||
<a href="#" class="btn btn-default" title="Flip Vertical" data-event="flip" data-value="vertical" data-y="1"><i class="fa fa-arrows-v"/></a>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="#" class="btn btn-default" title="Reset Image" data-event="reset"><i class="fa fa-refresh"/> Reset Image</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!--=================-->
|
||||
<!-- Snippet options -->
|
||||
<!--=================-->
|
||||
|
||||
Reference in New Issue
Block a user