/*
* Copyright 2019 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This dialog box enables to crop an image.<br>
*/
Ext.define('Ametys.helper.crop.CropDialog', {
alternateClassName: ['Ametys.helper.CropDialog'],
extend: 'Ametys.window.DialogBox',
/**
* @cfg {String} imageId (required) The id of the image to be cropped.
*/
/**
* @cfg {String} imageName (required) The name of the image to be cropped.
*/
/**
* @cfg {Number} imageSize (required) The size in bytes of the image to be cropped.
*/
/**
* @cfg {String} imageViewHref (required) A URL to view the image.
*/
/**
* @cfg {String} imageDownloadHref (required) A URL to download the image.
*/
/**
* @cfg {Object} cropUrl The URL for cropping
* @cfg {String} cropUrl.plugincore The plugin for the crop URL
* @cfg {String} [cropUrl.url="upload/crop-and-store"] The URL for cropping
*/
/**
* @private
* @property {String} _cropPlugin The plugin for the crop URL
*/
/**
* @private
* @property {String} _cropUrl The URL for cropping
*/
/**
* @cfg {Function} afterCropFn The callback function to execute after the crop has been made, after the 'Ok' button was clicked.
* @param {String} id The uploaded image id.
* @param {String} filename The uploaded image name.
* @param {Number} fileSize The uploaded image size in bytes.
* @param {String} viewHref A URL to view the image.
* @param {String} downloadHref A URL to download the image.
* @param {String} type The type of the image resource. Can be null.
*/
/**
* @private
* @property {Function} _afterCropFn The callback function to execute after the crop has been made
*/
statics: {
/**
* @private
* @property {Number} __IMAGE_MAX_HEIGHT The max height for displaying the image
* @readonly
*/
__IMAGE_MAX_HEIGHT: 600,
/**
* @private
* @property {Number} __IMAGE_MAX_WIDTH The max width for displaying the image
* @readonly
*/
__IMAGE_MAX_WIDTH: 600
},
constructor: function(config)
{
Ext.applyIf(config, this._getDefaultConfig(config));
this.callParent(arguments);
var cropUrlCfg = config.cropUrl || {};
this._cropPlugin = cropUrlCfg.plugin || 'core';
this._cropUrl = cropUrlCfg.url || 'upload/crop-and-store';
this._afterCropFn = config.afterCropFn || Ext.emptyFn;
},
/**
* Gets the default configuration
* @param {Object} config The configuration.
* @param {String} config.imageId The id of the image to be cropped.
* @param {String} config.imageName The name of the image to be cropped.
* @param {Number} config.imageSize The size in bytes of the image to be cropped.
* @param {String} config.imageViewHref A URL to view the image.
* @param {String} config.imageDownloadHref A URL to download the image.
* @param {Object} config.actionResult The result of the upload action.
* @private
*/
_getDefaultConfig: function(config)
{
var fileId = config.imageId;
var filename = config.imageName;
var fileSize = config.imageSize;
var viewHref = config.imageViewHref;
var downloadHref = config.imageDownloadHref;
var actionResult = config.actionResult;
return {
title: "{{i18n PLUGINS_CORE_UI_WIDGET_CROP_IMAGE_DIALOG_TITLE}}",
iconCls: 'ametysicon-code-html-image decorator-ametysicon-desktop-scissors',
maxHeight: this.statics().__IMAGE_MAX_HEIGHT + 200,
width: this.statics().__IMAGE_MAX_WIDTH + 100,
bodyPadding: '10',
layout: {
type: "vbox"
},
defaults: {
width: '100%'
},
items: [{
xtype: 'component',
html: "{{i18n PLUGINS_CORE_UI_WIDGET_CROP_IMAGE_DIALOG_HINT1}}",
cls: 'a-text'
},
this._imageComponentCfg(viewHref, filename)
, {
xtype: 'component',
html: "<span class=\"ametysicon-sign-caution\"></span><span style=\"padding-left:5px;\">{{i18n PLUGINS_CORE_UI_WIDGET_CROP_IMAGE_DIALOG_HINT2}}</span>",
cls: 'a-text-warning',
style: {
textAlign: 'right'
}
}],
buttons: [{
text: "{{i18n PLUGINS_CORE_UI_WIDGET_CROP_IMAGE_DIALOG_BUTTON_OK}}",
reference: 'okButton',
handler: Ext.bind(this._onOkButtonClick, this, [fileId, filename, fileSize, viewHref, downloadHref, actionResult], 1),
scope: this
}, {
text: "{{i18n PLUGINS_CORE_UI_WIDGET_CROP_IMAGE_DIALOG_BUTTON_CANCEL}}",
handler: function(btn) { this.close(); },
scope: this
}],
referenceHolder: true,
defaultButton: 'okButton',
defaultButtonTarget: 'el',
closeAction: 'destroy',
listeners: {
'destroy': function() {
this.getLogger().info('Destroying Jcrop');
this._jcropApi.destroy();
this._jcropApi = null;
},
scope: this
}
};
},
/**
* @private
* Called when 'Ok' button is pressed
* @param {Ext.button.Button} btn The 'Ok' button
* @param {String} id The uploaded file id.
* @param {String} filename The uploaded file name.
* @param {Number} fileSize The uploaded file size in bytes.
* @param {String} viewHref A URL to view the file.
* @param {String} downloadHref A URL to download the file.
* @param {Object} actionResult The result of the upload action.
*/
_onOkButtonClick: function(btn, id, filename, fileSize, viewHref, downloadHref, actionResult)
{
var cropping = this._getCropping();
if (cropping === '100%')
{
this.close();
this._afterCropFn(id, filename, fileSize, viewHref, downloadHref, actionResult);
}
else
{
this._sendCropping(id, filename, fileSize, viewHref, downloadHref, actionResult, cropping);
}
},
/**
* @private
* Gets the configuration for the Image component
* @param {String} viewHref The href for the image
* @param {String} filename The image file name
* @return {Object} the configuration for the Image component
*/
_imageComponentCfg: function(viewHref, filename)
{
// we could have used https://developer.mozilla.org/en-US/docs/Web/API/URL to better manage URL, but for browser compatibility purpose, we have to do this :(
var src = viewHref + (viewHref.indexOf('?') === -1 ? '?' : '&') + 'maxHeight=' + this.statics().__IMAGE_MAX_HEIGHT + '&maxWidth=' + this.statics().__IMAGE_MAX_WIDTH;
return {
xtype: 'container',
itemId: 'imageContainer',
layout: {
type: "vbox",
align: "middle"
},
items: [{
// we do not use xtype:'image' component as it will break layout when plugin Jcrop will modify the <img> element and add elements next to it
xtype: 'component',
itemId: 'image',
padding: '15 0 10 0',
html: '<img src="' + src + '" alt="' + filename + '">',
listeners: {
'afterrender': this._afterImageComponentRendering,
scope: this
}
}]
};
},
/**
* @private
* Callback called after the image component is rendered.
* @param {Ext.Component} cmp The image component
*/
_afterImageComponentRendering: function(cmp)
{
var cmpEl = cmp.getEl();
var cmpDom = cmpEl.dom;
var imageHtmlElement = cmpDom.tagName.toLowerCase() === 'img' ? cmpDom : cmpEl.down('img', true);
var me = this;
var logger = this.getLogger();
if (imageHtmlElement.complete)
{
onImageLoaded();
}
else
{
imageHtmlElement.addEventListener('load', onImageLoaded);
}
function onImageLoaded()
{
logger.info('Image loaded by browser');
cmp.updateLayout();
attachJcrop();
}
function attachJcrop()
{
var imageWidth = imageHtmlElement.width;
var imageHeight = imageHtmlElement.height;
me._attachJcrop.apply(me, [imageHtmlElement, cmp, imageWidth, imageHeight]);
}
},
/**
* @private
* Attaches the Jcrop plugin to the image component
* @param {HTMLImageElement} imageHtmlElement The HTML element for the image
* @param {Ext.Component} cmp The image component
* @param {Number} imageWidth The image width
* @param {Number} imageHeight The image height
*/
_attachJcrop: function(imageHtmlElement, cmp, imageWidth, imageHeight)
{
var me = this;
this.getLogger().info('Attaching Jcrop plugin');
var options = {
bgFade: true,
bgOpacity: 0.2,
bgColor: 'white'
};
jQuery(imageHtmlElement).Jcrop(options, function() {
me._jcropApi = this;
cmp.updateLayout();
});
},
/**
* @private
* Gets the current cropping. Values are between 0 and 1 (as ratio)
* @return {String/Object} The current cropping. Is a String valued to '100%' if it's a full image cropping, is an object (containing keys x1, y1, w, h) otherwise.
*/
_getCropping: function()
{
var bounds = this._jcropApi.getBounds();
var maxWidth = bounds[0];
var maxHeight = bounds[1];
var coords = this._jcropApi.tellSelect();
if (coords.x === 0 && coords.y === 0 && coords.w === maxWidth && coords.h === maxHeight /* jcrop selection is full */
|| coords.w === 0 && coords.h === 0 /* jcrop selection is released */)
{
return '100%';
}
var relativeX1 = coords.x / maxWidth;
var relativeY1 = coords.y / maxHeight;
// var relativeX2 = coords.x2 / maxWidth;
// var relativeY2 = coords.y2 / maxHeight;
var relativeWidth = coords.w / maxWidth;
var relativeHeight = coords.h / maxHeight;
return {
x1: relativeX1,
y1: relativeY1,
w: relativeWidth,
h: relativeHeight
};
},
/**
* @private
* Send the cropping values to the server, which will effectively crop the original image
* @param {String} id The uploaded file id.
* @param {String} filename The uploaded file name.
* @param {Number} fileSize The uploaded file size in bytes.
* @param {String} viewHref A URL to view the file.
* @param {String} downloadHref A URL to download the file.
* @param {Object} actionResult The result of the upload action.
* @param {Object} cropping The cropping object
* @param {Number} cropping.x1 The X coordinate of the upper-left corner of the rectangular cropping region
* @param {Number} cropping.y1 The Y coordinate of the upper-left corner of the rectangular cropping region
* @param {Number} cropping.w The width of the rectangular cropping region
* @param {Number} cropping.h The height of the rectangular cropping region
*/
_sendCropping: function(id, filename, fileSize, viewHref, downloadHref, actionResult, cropping)
{
Ametys.data.ServerComm.send({
plugin: this._cropPlugin,
url: this._cropUrl,
parameters: {
imageId: id,
x1: cropping.x1,
y1: cropping.y1,
width: cropping.w,
height: cropping.h
},
callback: {
handler: this._afterCroppingSent,
scope: this
},
responseType: 'text',
waitMessage: {
msg: "{{i18n PLUGINS_CORE_UI_FILEUPLOAD_SUBMITFORM_MSG}}",
target: this,
style: {
zIndex: 1000 // in order to be above the jcrop-tracker (which is at z-index: 360)
}
},
errorMessage: {
msg: "{{i18n PLUGINS_CORE_UI_FILEUPLOAD_ERROR_ON_SERVER}}"
}
});
},
/**
* @private
* Callback called after the server successfully cropped the original image with the given values.
* @param {Object} response The response
* @param {Object[]} arguments The arguments
*/
_afterCroppingSent: function(response, arguments)
{
var result = JSON.parse(response.textContent);
this.close();
this._afterCropFn(result.id, result.filename, result.size, Ametys.CONTEXT_PATH + result.viewHref, Ametys.CONTEXT_PATH + result.downloadHref, result);
}
});