/*
* Copyright 2010 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 panel displays editable properties in a table
*
* Creates your own panel class by inheriting this one and define at least the following methods:
* #saveProperty, #deleteProperty, #downloadBinaryProperty
* @private
*/
Ext.define('Ametys.repository.tool.NodePropertiesTool.TablePropertiesPanel',
{
extend: 'Ext.panel.Panel',
/**
* @cfg {Boolean} [showPendingChanges=true] Set to false to disable pending changes ui
*/
showPendingChanges: true,
/**
* @cfg {Boolean} [enableEdition=true] Set to false to disable properties' edition
*/
enableEdition: true,
/**
* @cfg {Boolean} [enableDeletion=true] Set to false to disable properties' deletion
*/
enableDeletion: true,
/**
* @cfg {String[]} binaryTypes The list of types considered as binary
*/
binaryTypes: ['binary'],
collapsible: true,
titleCollapse: true,
animCollapse: true,
header: {
titlePosition: 1
},
ui: 'light',
cls: 'table-properties-panel',
border: false,
defaults: {
// applied to each contained panel
bodyStyle: 'padding:10px'
},
bodyPadding: '10',
layout: {
type: 'table',
columns: 5
},
/**
* (Re-)Draw table's rows for each properties from XML nodes
* @param {Object[]} properties the XML node properties
*/
updatePropertiesFromXml: function (properties)
{
var me = this;
this.removeAll();
Ext.Array.each(properties, function (property) {
var name = property.getAttribute('name');
var type = property.getAttribute('type');
var multiple = property.getAttribute('multiple') == 'true';
var editable = property.getAttribute('editable') == 'true';
var isNew = property.getAttribute('new') == 'true';
var isModified = property.getAttribute('modified') == 'true';
var values = [];
if (multiple)
{
var nodeValues = Ext.dom.Query.select('> value', property);
for (var j = 0; j < nodeValues.length; j++)
{
values[j] = nodeValues[j].firstChild ? nodeValues[j].firstChild.nodeValue : '';
}
}
else
{
values[0] = Ext.dom.Query.selectValue('> value', property, '');
}
me._insertRow(name, type, values, multiple, editable, isNew, isModified);
});
},
/**
* @private
* Updates the HTML value and hide form
* @param {String} name The property name
* @param {Object} value The raw value
*/
_updateReadableValue: function (name, value)
{
var form = this._getCmp(name + '_form');
if (this.showPendingChanges)
{
this._getCmp(name + '_pendingchanges').show();
this._getCmp(name + '_label').addCls('modified');
}
var valueCmp = this._getCmp(name + '_value');
var inputFd = this._getCmp(name + '_input');
var html = '';
var type = inputFd.type.toLowerCase();
if (Ext.Array.contains(this.binaryTypes, type))
{
html = Ametys.repository.utils.Utils.formatBinaryValue([value], inputFd.multiple);
}
else
{
switch (type)
{
case 'date':
html = Ametys.repository.utils.Utils.formatDateValue([value]);
break;
case 'reference':
case 'weakreference':
html = Ametys.repository.utils.Utils.formatReferenceValue(typeof (value) == 'string' ? value.split(/\r?\n/g) : ['' + value], inputFd.multiple);
break;
case 'string':
var values = inputFd.multiple ? value : ['' + value];
html = Ametys.repository.utils.Utils.formatStringValue(typeof (value) == 'string' ? value.split(/\r?\n/g) : values, inputFd.multiple, true);
break;
case 'path':
case 'name':
case 'double':
case 'long':
case 'boolean':
var values = inputFd.multiple ? value : ['' + value];
html = Ametys.repository.utils.Utils.formatStringValue(typeof (value) == 'string' ? value.split(/\r?\n/g) : values, inputFd.multiple);
break;
default:
html = '<span class="not-supported">The #' + type + ' type is not supported for display</span>';
break;
}
}
valueCmp.update(html);
valueCmp.value = typeof (value) == 'string' ? value.split(/\r?\n/g) : value;
form.hide();
valueCmp.show();
},
/**
* @protected
* @template
* Do save the property.
* You have to implement this method.
* @param {Ext.form.Panel} form The form panel to submit the value
* @param {String} type The property type
* @param {String} id The id of form field.
* @param {String} name The property name.
* @param {String} value The property new value as string
* @param {Function} [successCb] The callback function to call in case of success
* @param {Function} [failureCb] The callback function to call in case of failure
*/
saveProperty: function (form, type, id, name, value, successCb, failureCb)
{
throw new Error("Method #saveProperty is not implemented");
},
/**
* @protected
* @template
* Do delete the property.
* You have to implement this method.
* @param {String} name The property's name
* @param {Function} [successCb] The callback function to call in case of success
* @param {Function} [failureCb] The callback function to call in case of failure
*/
deleteProperty: function (name, successCb, failureCb)
{
throw new Error("Method #deleteProperty is not implemented");
},
/**
* @protected
* @template
* Download a binary property. You have to implement this method.
* @param {String} name The property's name
* @param {String} type The property's type
*/
downloadBinaryProperty: function (name, type)
{
throw new Error("Method #downloadBinaryProperty is not implemented");
},
/**
* @protected
* @template
* Function invoked when cliking on a 'reference'/'weakreference' value
* @param {String} value the value
*/
onClickReferenceProperty: function (value)
{
throw new Error("Method #onClickReferenceProperty is not implemented");
},
/**
* @private
* Insert the new row
* @param {String} name The property's name.
* @param {String} type The property's type.
* @param {String[]} values The property's values as a array of string.
* @param {Boolean} multiple true if multiple
* @param {Boolean} editable true if editable
* @param {Boolean} isNew true if new
* @param {Boolean} isModified true if has been modified
*/
_insertRow: function(name, type, values, multiple, editable, isNew, isModified)
{
var components = [];
if (this.showPendingChanges)
{
// Pending changes icon
components.push({
xtype: 'container',
items: [{
cls: 'property-btn-unactive',
itemId: this._escapeId(name + '_pendingchanges'),
hidden: !(isNew || isModified),
border: false,
xtype: 'button',
width: 30,
iconCls: 'ametysicon-clock169',
tooltip: "{{i18n PLUGINS_REPOSITORYAPP_PROPERTY_PENDING_CHANGES}}"
}]
});
}
else
{
// Empty components
components.push({
xtype: 'component'
});
}
if (this.enableDeletion && editable)
{
// Delete property button
components.push({
cls: 'property-btn',
border: false,
xtype: 'button',
width: 30,
iconCls: 'ametysicon-delete30',
tooltip: "{{i18n PLUGINS_REPOSITORYAPP_PROPERTY_REMOVE_BUTTON_TEXT}}",
handler: Ext.bind(this.deleteProperty, this, [name], false),
scope: this
});
}
else
{
// Empty components
components.push({
xtype: 'component'
});
}
if (this.enableEdition && editable)
{
// Edit property button
components.push({
cls: 'property-btn',
xtype: 'button',
border: false,
iconCls: 'ametysicon-edit5',
width: 30,
tooltip: "{{i18n PLUGINS_REPOSITORYAPP_PROPERTY_EDIT_BUTTON_TEXT}}",
handler: Ext.bind(this._editProperty, this, [name], false),
scope: this
});
}
else
{
components.push({
xtype: 'component'
});
}
// Property label
components.push({
xtype: 'component',
itemId: this._escapeId(name + '_label'),
flex: 0.4,
cls: 'property-label' + (isNew || isModified ? ' modified' : ''),
html: "<div><b>" + name + "</b> (" + type + ")</div>"
});
// Property value
var valueCmp = {
itemId: this._escapeId(name + '_value'),
xtype: 'component',
cls: 'property-value' + (!editable ? '-disabled': ''),
flex: 1,
html: this._formatValue(type, values, multiple),
value: Ametys.repository.utils.Utils.getMultipleValueDisplay(values, '\n'),
listeners: {
render: {
fn: function(comp, eOpts) {
type = type.toLowerCase();
if (type == 'reference' || type == 'weakreference')
{
comp.mon(comp.getEl(), 'click', function () { this.onClickReferenceProperty(values[0]) }, this);
}
else if (Ext.Array.contains(this.binaryTypes, type))
{
comp.mon(comp.getEl(), 'click', function () { this.downloadBinaryProperty(name, type) }, this);
}
if (editable && type != 'reference' && type != 'weakreference')
{
comp.mon(comp.getEl(), 'dblclick', function () { this._editProperty(name) }, this);
}
},
scope: this
}
}
};
var form = Ext.create('Ext.form.Panel', {
itemId: this._escapeId(name + '_form'),
defaults: {
width: 400
},
border: false,
hidden: true,
listeners: {
'show': function(form) {
// FIXME File-field workaround
var fileField = form.down('filefield');
if (fileField != null)
{
fileField.ametysSaved = false;
fileField.ametysChanged = false;
}
},
'hide': function(form) {
// var fileField = form.down('filefield');
// if (fileField != null)
// {
// fileField.ametysChanged = false;
// }
}
}
});
components.push({
xtype: 'panel',
itemId: this._escapeId(name + '_valuePanel'),
border: false,
items: [
valueCmp,
form
]
});
if (this.enableEdition && editable)
{
var editField = this._getEditionField(type, name, values, multiple);
if (editField != null)
{
form.add(editField);
}
}
this.add(components);
},
/**
* Get a string representation of a property value.
* @param {String} type The property type.
* @param {Array} values The property values.
* @param {Boolean} multiple true if the property is multi-valued, false otherwise.
* @return {String} the HTML property representation.
* @private
*/
_formatValue: function(type, values, multiple)
{
type = type.toLowerCase();
if (Ext.Array.contains(this.binaryTypes, type))
{
return Ametys.repository.utils.Utils.formatBinaryValue(values, multiple);
}
else
{
switch (type.toLowerCase())
{
case 'date':
return Ametys.repository.utils.Utils.formatDateValue(values, multiple);
case 'reference':
case 'weakreference':
return Ametys.repository.utils.Utils.formatReferenceValue(values, multiple);
case 'string':
return Ametys.repository.utils.Utils.formatStringValue(values, multiple, true);
case 'name':
case 'path':
case 'double':
case 'long':
case 'boolean':
return Ametys.repository.utils.Utils.formatStringValue(values, multiple);
default:
return '<span class="not-supported">The #' + type + ' type is not supported for display</span>';
}
}
},
/**
* @protected
* Get the field for editing property
* @param {String} type The type
* @param {String} name The property name
* @param {Object} values The property's values
* @param {Boolean} multiple true if the property is multiple
* @return {Ext.form.field.Base} The form field
*/
_getEditionField: function (type, name, values, multiple)
{
type = type.toLowerCase();
if (Ext.Array.contains(this.binaryTypes, type))
{
return this._getBinaryField(name, type, values);
}
else
{
switch (type)
{
case 'string':
case 'name':
case 'path':
case 'reference':
case 'weakreference':
return this._getStringField(name, type, values, multiple);
case 'date':
return this._getDateField(name, type, values, multiple);
case 'boolean':
return this._getBooleanField(name, type, values, multiple);
case 'long':
return this._getLongField(name, type, values, multiple);
case 'double':
return this._getDoubleField(name, type, values, multiple);
default:
this.getLogger().info('[' + this.self.getName() + '] Type #' + type + ' is not supported for edition');
return null;
}
}
},
/**
* The field configuration for a string property
* @param {String} name The property name.
* @param {String} type The property type.
* @param {Array} values The property values.
* @param {Boolean} multiple true if the property is multi-valued, false otherwise.
* @return {Ext.form.field.Base} the input field.
* @private
*/
_getStringField: function(name, type, values, multiple)
{
var cfg = {
xtype: multiple ? 'textarea' : 'textfield',
hideLabel: true,
itemId: this._escapeId(name + '_input'),
name: name,
type: type,
enableKeyEvents: true,
multiple: multiple,
listeners: {
'specialkey': {fn: this._onKeyPress, scope: this},
'blur': {fn: this._validEdit, scope: this}
}
}
if (multiple)
{
cfg.value = Ametys.repository.utils.Utils.getMultipleValueDisplay(values, '\n', true);
}
else
{
cfg.value = values[0];
}
return cfg;
},
/**
* The field configuration for a string property
* @param {String} name The property name.
* @param {String} type The property type.
* @param {Array} values The property values.
* @return {Ext.form.FieldContainer} the input field.
* @private
*/
_getDateField: function(name, type, values)
{
var value = values[0];
if (!Ext.isDate(values[0]))
{
value = Ext.Date.parse(values[0], Ext.Date.patterns.ISO8601DateTime);
}
return {
xtype: 'datetimefield',
hideLabel: true,
itemId: this._escapeId(name + '_input'),
name: name,
type: type,
enableKeyEvents: true,
allowBlank: false,
value: value,
listeners: {
'specialkey': {fn: this._onKeyPress, scope: this},
'blur': {fn: this._validEdit, scope: this}
}
}
},
/**
* Draw an input field for a Long property.
* @param {String} name The property name.
* @param {String} type The property type.
* @param {Array} values The property values.
* @param {Boolean} multiple true if the property is multi-valued, false otherwise.
* @return {Ext.form.field.Base} the input field.
* @private
*/
_getLongField: function(name, type, values, multiple)
{
var cfg = {
hideLabel: true,
itemId: this._escapeId(name + '_input'),
name: name,
type: type,
enableKeyEvents: true,
allowBlank: false,
listeners: {
'specialkey': {fn: this._onKeyPress, scope: this},
'blur': {fn: this._validEdit, scope: this}
}
}
if (!multiple)
{
return Ext.apply(cfg, {
xtype: 'numberfield',
allowDecimals: false,
value: values[0]
});
}
else
{
return Ext.apply(cfg, {
xtype: 'textarea',
multiple: true,
value: Ametys.repository.utils.Utils.getMultipleValueDisplay(values, '\n', true)
});
}
},
/**
* Draw an input field for a double property.
* @param {String} name The property name.
* @param {String} type The property type.
* @param {Array} values The property values.
* @param {Boolean} multiple true if the property is multi-valued, false otherwise.
* @return {Ext.form.field.Base} the input field.
* @private
*/
_getDoubleField: function(name, type, values, multiple)
{
var cfg = {
hideLabel: true,
itemId: this._escapeId(name + '_input'),
name: name,
type: type,
enableKeyEvents: true,
allowBlank: false,
listeners: {
'specialkey': {fn: this._onKeyPress, scope: this},
'blur': {fn: this._validEdit, scope: this}
}
}
if (!multiple)
{
return Ext.apply(cfg, {
xtype: 'numberfield',
allowDecimals: true,
value: values[0]
});
}
else
{
return Ext.apply(cfg, {
xtype: 'textarea',
multiple: true,
value: Ametys.repository.utils.Utils.getMultipleValueDisplay(values, '\n', true)
});
}
},
/**
* Draw an input field for a Boolean property.
* @param {String} name The property name.
* @param {String} type The property type.
* @param {Array} values The property values.
* @return {Ext.form.field.Base} the input field.
* @private
*/
_getBooleanField: function(name, type, values)
{
return {
xtype: 'combobox',
hideLabel: true,
itemId: this._escapeId(name + '_input'),
name: name,
triggerAction: 'all',
type: type,
width: 70,
store: ['true', 'false'],
enableKeyEvents: true,
value: values[0],
listeners: {
'specialkey': {fn: this._onKeyPress, scope: this},
'blur': {fn: this._validEdit, scope: this}
}
}
},
/**
* Draw an input field for a Binary property.
* @param {String} name The property name.
* @param {String} type The property type.
* @param {Array} values The property values.
* @return {Ext.form.field.Base} the input field.
* @private
*/
_getBinaryField: function(name, type, values)
{
return {
xtype: 'filefield',
hideLabel: true,
itemId: this._escapeId(name + '_input'),
name: name,
value: values[0],
type: type,
enableKeyEvents: true,
listeners: {
'specialkey': {fn: this._onKeyPress, scope: this},
// FIXME File-field workaround
'blur': {fn: this._onBlurBinary, scope: this},
'change': {fn: this._onChangeBinary, scope: this}
}
}
},
/**
* Listens for enter and escape keys to save the property or cancel edition mode.
* @param {Ext.form.field.Base} field The field on which the key was pressed.
* @param {Ext.event.Event} event The keypress event.
* @param {Object} eOpts Options added to the addListener
* @private
*/
_onKeyPress: function(field, event, eOpts)
{
if (event.getKey() == event.ENTER && !field.multiple)
{
this._validEdit(field);
}
else if (event.getKey() == event.ESC)
{
this._cancelEdit(field);
}
},
/**
* @private
* Listener when the binary field is blured
* @param {Ext.form.field.File} field The blured field
* @param {Ext.event.Event} event The blur event
*/
_onBlurBinary: function(field, event)
{
// FIXME File-field workaround
if (field.ametysChanged === true && field.getValue() != '')
{
this._validEdit(field);
}
},
/**
* @private
* Listener when the binary field value has changed
* @param {Ext.form.field.File} field The field
* @param {String} newValue The new value of the field
*/
_onChangeBinary: function(field, newValue)
{
// FIXME File-field workaround
field.ametysChanged = true;
},
/**
* Validate and save the new property value.
* @param {Ext.form.field.Base} field The property input field.
* @private
*/
_validEdit: function(field)
{
var name = field.getName();
var value = field.fileInput != null ? field.fileInput.getValue() : field.getValue();
var form = this._getCmp(name + '_form');
if (!form.hidden)
{
var valueCt = this._getCmp(name + '_value');
if (valueCt.value == value)
{
form.hide();
valueCt.show();
}
else
{
if (field.multiple && !Ext.isArray(value))
{
value = value.split("\n");
}
if (Ext.isDate(value))
{
value = Ext.Date.format (value, Ext.Date.patterns.ISO8601DateTime);
}
this.saveProperty(form, field.type, field.getId(), name, value, field.multiple, Ext.bind( this._updateReadableValue, this, [name, value]));
// if (!field.isXType('filefield') || field.saved === false)
// if (!field.isXType('filefield') || field.saved === false)
// {
// field.saved = true;
// }
}
}
},
/**
* Inline-edit a property: hide the value field and show the input field instead.
* @param {String} name The property name.
* @private
*/
_editProperty: function(name)
{
var form = this._getCmp(name + '_form');
if (form != null)
{
form.show();
var valueCt = this._getCmp(name + '_value');
valueCt.hide();
var inputFd = this._getCmp(name + '_input');
inputFd.focus();
}
},
/**
* Cancel edition mode: hide the input field and show the value field.
* @param {Ext.form.field.Base} field The property input field.
* @private
*/
_cancelEdit: function(field)
{
var name = field.getName();
var form = this._getCmp(name + '_form');
var valueCt = this._getCmp(name + '_value');
field.setValue(valueCt.value);
form.hide();
valueCt.show();
},
/**
* Get a component under the properties panel (at any level) by its ID.
* @param {String} id the component ID (unescaped).
* @return {Ext.Component} the component.
*/
_getCmp: function(id)
{
return this.down('#' + this._escapeId(id));
},
/**
* Escape a string to use as a component ID.
* @param {String} id The string to use as ID.
* @return {String} The escaped ID.
*/
_escapeId: function(id)
{
// prefix with id to ensure item id always starts with a letter
return 'id-' + id.replace(/[^a-z0-9\-_]/ig, '_');
}
});