/*
* Copyright 2022 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 class provides a widget to select an image
*/
Ext.define('Ametys.form.widget.Image', {
extend: 'Ametys.form.AbstractField',
/** @cfg {String} [deleteButtonIcon] The full path to the delete button icon (in 16x16 pixels). Can be null to use #cfg-deleteButtonIconCls instead */
/** @cfg {String} [deleteButtonIconCls=ametysicon-delete30] The CSS class to apply to delete button icon */
deleteButtonIconCls: 'ametysicon-delete30',
/** @cfg {String} [shape=rectangle] Can be 'rectangle', 'square' or 'circle' */
/** @cfg {String|String[]} [allowSources="external"] The allowed file sources in a Array or separated by comma. Default to 'external'. */
/** @cfg {Number} border=2 The border width */
border: 3,
/** @cfg {String} noImage Url when there is no image */
noImage: Ametys.getPluginResourcesPrefix('core-ui') + '/img/Ametys/common/form/widget/image/noimage.svg',
constructor: function (config)
{
config.shape = config.shape || 'rectangle';
config.cls = ['ametys-widget', 'ametys-widget-image', 'ametys-widget-image-shape-' + config.shape];
if (config.shape == 'circle' && config.shadow === undefined)
{
config.shadow = false;
}
config.imageWidth = config.width || 100;
config.imageHeight = config.height || 100;
if (config.shape == 'square' || config.shape == 'circle')
{
config.imageHeight = config.imageWidth;
}
config.width = config.imageWidth + this.border * 2;
config.height = config.imageHeight + this.border * 2;
// File location
if (Ext.isEmpty(config['allowSources']))
{
/**
* @protected
* @cfg {String[]} [defaultSources=[Ametys.form.widget.File.External.SOURCE]] The sources to use when {#cfg-allowSources} is null or empty
*/
this.fileSources = config.defaultSources || [Ametys.form.widget.File.External.SOURCE];
}
else
{
this.fileSources = Ext.isArray(config['allowSources']) ? config['allowSources'] : config['allowSources'].split(',');
}
delete config.allowSources;
this.callParent(arguments);
this.on('boxready', function() {
if (this.autoClick && this._bbar.items.length == 2 && !this._bbar.items.get(1).isVisible())
{
// auto click activated
// + only one button (ignoring the hidden delete button)
this._usedAutoClick = true;
this._bbar.items.get(0).click();
}
}, this);
},
disable: function()
{
this.callParent(arguments);
this._updateDisable();
},
enable: function()
{
this.callParent(arguments);
this._updateDisable();
},
/**
* Sets the read only state of this field.
* @param {Boolean} readOnly New status
*/
setReadOnly: function(readOnly)
{
this._bbar.setVisible(!readOnly);
},
/**
* @private
* Update the enable status
*/
_updateDisable: function() {
if (this._bbar.rendered)
{
var me = this;
this._bbar.items.each(function(elt) {
elt.setDisabled(me.disabled);
});
}
},
setMinWidth: function(width)
{
this.callParent(arguments);
if (this.rendered && width > this.getWidth()
|| !this.rendered && width > this.width)
{
this.setWidth(width);
}
},
setWidth: function(width)
{
this.imageWidth = width - this.border * 2;
if (this.shape == 'square' || this.shape == 'circle')
{
this.imageHeight = this.imageWidth;
}
this.callParent(arguments);
this._updateSize();
},
setMinHeight: function(height)
{
this.callParent(arguments);
if (this.rendered && height > this.getHeight()
|| !this.rendered && height > this.height)
{
this.setHeight(height);
}
},
setHeight: function(height)
{
this.imageHeight = height - this.border * 2;
if (this.shape == 'square' || this.shape == 'circle')
{
this.imageWidth = this.imageHeight;
}
this.callParent(arguments);
this._updateSize();
},
/**
* @private
* Update the size
*/
_updateSize: function()
{
this._container.setWidth(this.imageWidth + this.border * 2); // image width + padding
this._container.setHeight(this.imageHeight + this.border * 2); // image height + padding
if (this._img.rendered)
{
this._img.el.dom.style.lineHeight = (this.imageHeight - 2) + "px";
this._img.imgEl.dom.style.width = this.imageWidth + "px";
this._img.imgEl.dom.style.height = this.imageHeight + "px";
this._img.imgEl.dom.style.maxWidth = this.imageWidth + "px";
this._img.imgEl.dom.style.maxHeight = this.imageHeight + "px";
this._img.imgEl.dom.style.objectFit = 'contain';
}
if (this._bbar.rendered)
{
var maxPercentSize = 0.33;
var maxPixelSize = 50;
var externalSeparatorWeight = 2;
var maxInternalSeparatorPixelSize = 10;
var glyphSize = 0.33;
var minSize = Math.min(this.imageWidth, this.imageHeight);
var nbButtons = this.fileSources.length + 1 /* delete */;
var buttonSize = Math.min(Math.min(1 / (nbButtons + 1 /* space */), maxPercentSize) * minSize, maxPixelSize);
var separatorSize = Math.min((this.imageWidth - nbButtons * buttonSize) / (2 * nbButtons + 2 * externalSeparatorWeight), maxInternalSeparatorPixelSize);
this._bbar.items.each(function(elt) {
elt.setWidth(buttonSize);
elt.setHeight(buttonSize);
elt.setStyle({ fontSize: (buttonSize * glyphSize) + 'px' })
elt.setMargin('0 ' + separatorSize + ' 0 ' + separatorSize);
});
this._bbar.setLocalXY(0, ((this.imageHeight + this.border * 2) - buttonSize) * 3 / 4);
this._bbar.setWidth(this.imageWidth + this.border * 2);
this._bbar.setHeight(buttonSize);
}
// Change image preview url
this._updateUI();
},
initComponent: function()
{
this.items = this.items || [];
this._container = Ext.create('Ext.container.Container', {
layout: 'absolute'
});
this.items.push(this._container);
this._createPreviewImage();
this._createButtons();
this.callParent(arguments);
},
/**
* @private
* Creates the image preview image
*/
_createPreviewImage: function()
{
/**
* @property {Ext.Img} _img Displaying the selected image OR the empty image
* @private
*/
this._img = Ext.create('Ext.Img', {
autoEl: 'div',
src: this.noImage,
width: '100%',
height: '100%',
listeners: {
'afterrender': {
fn: this._updateSize,
scope: this,
options: { single: true }
}
}
});
this._container.add(this._img);
},
/**
* @private
* Creates the button to insert local image
*/
_createButtons: function()
{
var buttons = [];
var buttonCfg = {
text: "",
disabled: this.disabled,
scope: this
};
for (var i=0; i < this.fileSources.length; i++)
{
var source = Ametys.form.widget.File.getFileSource(this.fileSources[i]);
var itemCfg = source.getMenuItemConfig (this.getInitialConfig(), Ametys.form.widget.File.IMAGE_FILTER);
itemCfg.tooltip = itemCfg.text + (itemCfg.tooltip ? "<br/>" + itemCfg.tooltip : "");
itemCfg.text = "";
itemCfg.handler = this._selectFile;
Ext.applyIf(itemCfg, buttonCfg);
buttons.push(Ext.create("Ext.button.Button", itemCfg));
}
this._deleteButton = Ext.create("Ext.button.Button", Ext.applyIf({
icon: this.deleteButtonIcon,
iconCls: this.deleteButtonIcon ? null : this.deleteButtonIconCls,
/** @cfg {String} deleteTextConfirm The text to display when deleting a file. */
tooltip: this.deleteText || Ametys.form.widget.File.filters[Ametys.form.widget.File.IMAGE_FILTER].deleteText,
handler: this._deleteFile,
hidden: true
}, buttonCfg));
buttons.push(this._deleteButton);
/**
* @private
* @property {Ext.Container} _bbar The parent of action buttons
*/
this._bbar = Ext.create("Ext.Container", {
layout: {
type: 'hbox',
pack: 'center'
},
/**
* @cfg {Boolean} readOnly true to prevent the user from changing the field
*/
hidden: this.readOnly,
items: buttons,
listeners: {
'afterrender': {
fn: this._updateSize,
scope: this,
options: { single: true }
}
}
});
this._container.add(this._bbar);
},
/**
* Select a file.
* @param {Ext.Button} btn The button calling this function
* @protected
*/
_selectFile: function (btn)
{
this.triggerDialogBoxOpened = true;
var source = Ametys.form.widget.File.getFileSource(btn.source);
source.handler (this.getInitialConfig(), Ametys.form.widget.File.IMAGE_FILTER, null, Ext.bind(this._insertResourceCb, this, [source.getFileType()], true));
},
/**
* Callback function, called when a resource is uploaded in the dialog.
* @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. Can be null.
* @param {String} type The type of the resource. Can be null.
* @protected
*/
_insertResourceCb: function(id, filename, fileSize, viewHref, downloadHref, actionResult, type)
{
if (id == null)
{
return;
}
this.setValue({
id: id,
filename: filename,
size: fileSize,
viewUrl: viewHref,
downloadUrl: downloadHref,
type: type
});
this.triggerDialogBoxOpened = false;
this.focus();
},
/**
* Delete the file.
* @private
*/
_deleteFile: function()
{
this.triggerDialogBoxOpened = true;
// Show the confirmation dialog.
Ametys.Msg.confirm(
this.getInitialConfig('deleteText') || Ametys.form.widget.File.filters[Ametys.form.widget.File.IMAGE_FILTER].deleteText,
this.getInitialConfig('deleteTextConfirm') || Ametys.form.widget.File.filters[Ametys.form.widget.File.IMAGE_FILTER].deleteTextConfirm,
this._effectiveDeletFile,
this
);
},
/**
* Delete the file.
* @private
*/
_effectiveDeletFile: function(btn)
{
if (btn == 'yes')
{
this.setValue();
}
this.triggerDialogBoxOpened = false;
this.focus();
},
/**,
* Sets a data value into the field.
* @param {Object} value The value to set.
* @param {String} [value.id] The file identifier.
* @param {String} value.filename The file name (if applicable).
* @param {String} value.type The file object type ('metadata').
* @param {Number} value.size The file size in bytes.
* @param {Number} value.lastModified The file's last modification date.
* @param {Number} value.viewUrl A URL to view the file.
* @param {Number} value.downloadUrl A URL to download the file.
*/
setValue: function (value)
{
if (value && !Ext.Object.isEmpty(value))
{
value = Ext.applyIf (value, {
id: 'untouched',
type: 'external'
});
}
else
{
value = {};
}
this.callParent([value]);
Ext.suspendLayouts();
try
{
this._updateUI();
}
finally
{
Ext.resumeLayouts(true);
}
},
isEqual: function(value1, value2)
{
return value1 != null && value2 != null && value1.id === value2.id;
},
getSubmitValue: function ()
{
return this.value ? Ext.encode(this.value) : '';
},
getReadableValue: function ()
{
return this.value ? this.value.filename : '';
},
/**
* Update UI
* @private
*/
_updateUI: function()
{
var value = this.value;
if (!value || !value.id)
{
this._deleteButton.hide();
this._img.setSrc(this.noImage);
}
else
{
if (!this.readOnly && this.allowBlank)
{
this._deleteButton.show();
}
var separator = '&';
if (value.viewUrl.indexOf('?') < 0)
{
separator = '?';
}
var maxWidth = Math.round(this.imageWidth);
var maxHeight = Math.round(this.imageHeight);
var imgSrc = value.viewUrl.replace('_max100x100', '_max' + maxHeight + 'x' + maxWidth) + separator + 'maxWidth=' + maxWidth + '&maxHeight=' + maxHeight + '&foo=' + Math.random();
this._img.setSrc(imgSrc);
}
}
});