/*
* Copyright 2016 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 repeater container
* @private
*/
Ext.define('Ametys.form.ConfigurableFormPanel.Repeater',
{
extend: 'Ext.panel.Panel',
alias: 'widget.cms.repeater',
statics: {
/**
* @property {Number} PADDING The padding of repeater
* @private
* @readonly
*/
PADDING: 5,
/**
* @property {Number} NESTED_OFFSET The left offset when nesting repeaters
* @private
* @readonly
*/
NESTED_OFFSET: 20,
/**
* @property {RegExp} HEADER_VARIABLES Regular expression used to extract used metadatas from the header template.
* @private
* @readonly
*/
HEADER_VARIABLES: /\{([^\}\:]+)(?:\:[^}]+)?\}/gi,
/**
* @property {Number} TOOLS_COLUMN_WIDTH The width for tools column for repeaters in mode table
* @private
* @readonly
*/
TOOLS_COLUMN_WIDTH: 91,
/**
* @property {Number} FIELD_MARGIN_RIGHT The margin right for fields
* @private
* @readonly
*/
FIELD_MARGIN_RIGHT: 5,
/**
* @property {Number} SIMPLE_FIELD_MAX_HEIGHT The max height for simple fieds in table mode
* @private
* @readonly
*/
SIMPLE_FIELD_MAX_HEIGHT: 24,
/**
* @protected
* Retrieves the name of the repeater at the given index
* @param {String} name The name
* @param {String} separator The separator
* @param {Number} index The index
* @return {String} The name of the repeater at the given index
*/
getNameAtIndex: function(name, separator, index)
{
return name + '[' + index + ']';
}
},
/**
* @cfg {String} label The repeater label.
*/
/**
* @cfg {Ametys.form.ConfigurableFormPanel} form The parent form panel
*/
/**
* @cfg {String} addLabel The add button label.
*/
/**
* @cfg {String} delLabel The delete button label.
*/
/**
* @cfg {String} headerLabel The item panel header label template.
*/
/**
* @cfg {Boolean} readOnly True to disallow add, delete and move actions
*/
/**
* @cfg {Number} minSize The repeater min size.
*/
/**
* @cfg {Number} maxSize The repeater max size.
*/
/**
* @cfg {HTMLElement} compositionNode The repeater composition as XML node.
*/
/**
* @cfg {Object} composition The repeater composition as JSON object.
*/
/**
* @cfg {Object/Object[]} fieldCheckers The field checkers of this repeater as a JSON object
*/
/**
* @cfg {String} prefix The attribute prefix (to create sub elements)
*/
/**
* @cfg {Number} nestingLevel The nesting level of the repeater.
*/
/**
* @cfg {String} invalidCls The CSS class to use when marking the repeater invalid.
*/
invalidCls: 'a-repeater-invalid',
/**
* @cfg {String} [defaultPathSeparator="/"] The default separator for fields
*/
defaultPathSeparator: '/',
/**
* @cfg {String} [mode="panel"] The mode to use for the repeater's modification. Can be panel (default) or table
*/
mode: 'panel',
/**
* @cfg {String/String[]/Ext.XTemplate} activeErrorsTpl The template used to format the Array of error messages.
* It renders each message as an item in an unordered list.
*/
activeErrorsTpl: [
'<tpl if="errors && errors.length">',
'<ul class="{listCls}">',
'<tpl for="errors"><li>{.}</li></tpl>',
'</ul>',
'</tpl>'
],
/**
* @property {Number} _lastInsertItemPosition The last inserted item was at this position
* @private
*/
/**
* @property {Object} _newItemExternalDisableConditionsValues the external disable conditions values to set to new repeater items
* @private
*/
/**
* @property {Boolean} isRepeater
* Flag denoting that this component is a Repeater. Always true.
*/
isRepeater : true,
constructor: function(config)
{
config = config || {};
if (config.defaultPathSeparator)
{
this.defaultPathSeparator = config.defaultPathSeparator;
}
this._newItemExternalDisableConditionsValues = config.newItemExternalDisableConditionsValues || {};
this.callParent(arguments);
},
initComponent: function()
{
Ext.apply(this, {
ui: 'light',
border: false,
shadow: false,
isRepeater: true,
margin: this.nestingLevel > 1 ? ('0 0 5 ' + Ametys.form.ConfigurableFormPanel.Repeater.NESTED_OFFSET) : '0 0 5 0',
items: [{
hidden: true,
items: [{
xtype: 'numberfield',
name: '_' + this.prefix + this.name + this.defaultPathSeparator + 'size',
value: 0
}]
}]
});
var header = {
title: this.label + ' (0)',
style: "border-width: 1px !important",
};
var addFirstTool = !this.readOnly ? this._addFirst(this.addLabel) : undefined;
var cls = 'a-repeater a-repeater-level-' + this.nestingLevel + (this.nestingLevel % 2 == 0 ? ' even' : ' odd');
if (this.mode == 'table')
{
Ext.applyIf(this, {
layout: {
type: 'anchor'
}
});
// Header line with columns' labels is put in dockedItems
var dockedItemsChildren = [];
// Tool to add a repeater item at first position
if (addFirstTool)
{
Ext.apply(addFirstTool, {
width: Ametys.form.ConfigurableFormPanel.Repeater.TOOLS_COLUMN_WIDTH,
minWidth: Ametys.form.ConfigurableFormPanel.Repeater.TOOLS_COLUMN_WIDTH
});
dockedItemsChildren.push(addFirstTool);
}
// Columns' labels
Ext.Array.push(dockedItemsChildren, this._getRepeaterTableColumns());
Ext.apply(this, {
header: header,
dockedItems: [{
xtype: 'container',
ui: 'light',
cls: 'a-column-header',
border: true,
layout: {
type: 'hbox',
align: 'stretch'
},
items: dockedItemsChildren
}],
cls: cls + ' a-repeater-table'
});
}
else
{
Ext.applyIf(this, {
layout: {
type: 'repeater-accordion',
multi: true
}
});
this._toolExpandAll = Ext.create('Ext.panel.Tool', {
type: 'expandall',
qtip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_TOOL_EXPAND_ALL_HINT}}",
handler: this.expandAll,
hidden: true,
scope: this
});
this._toolCollapseAll = Ext.create('Ext.panel.Tool', {
type: 'collapseall',
qtip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_TOOL_COLLAPSE_ALL_HINT}}",
handler: this.collapseAll,
hidden: true,
scope: this
});
var tools = [
this._toolExpandAll,
this._toolCollapseAll
];
if (addFirstTool)
{
tools.push(addFirstTool);
}
Ext.apply(header, {
titlePosition: 2
});
Ext.apply(this, {
header: header,
tools: tools,
cls: cls
});
}
if (this.headerLabel)
{
// Compile the header template and extract the metadata names.
this._headerTpl = new Ext.Template(this.headerLabel, {compiled: true});
this._headerFields = [];
while ((result = Ametys.form.ConfigurableFormPanel.Repeater.HEADER_VARIABLES.exec(this.headerLabel)) != null)
{
this._headerFields.push(result[1]);
}
}
// Monitor when the form is ready, to update panel headers accordingly.
if (this.form)
{
this.form.on('formready', this._onFormReady, this);
this.form.on('repeaterEntryReady', this._onRepeaterEntryReady, this);
}
/**
* @event validitychange
* Fires when an entry was added or deleted
* @param {Ametys.form.ConfigurableFormPanel.Repeater} this
* @param {Boolean} isValid Whether or not the repeater is now valid
*/
this.callParent(arguments);
},
/**
* @private
* Retrieves the items to initialize the header line of repeater as table
* @return the items to initialize header of repeater
*/
_getRepeaterTableColumns: function()
{
let items = [];
let data = this.composition;
for (let name in data)
{
if (!data[name] || data[name].hidden)
{
continue;
}
let item = data[name];
let innerItems = [];
let label = item.label;
let isMandatory = item.validation ? (item.validation.mandatory) || false : false;
let labelWithMandatory = label + (label && isMandatory ? '* ' : '');
innerItems.push({
xtype: 'component',
html: labelWithMandatory,
cls: 'a-text'
});
let description = item.description;
let help = item.help;
if (description)
{
innerItems.push({
xtype: 'component',
cls: 'ametys-description',
listeners: {
afterrender: function(me)
{
// Register the new tip with an element's ID
Ext.tip.QuickTipManager.register({
target: me.getId(), // Target button's ID
text: description, // Tip content
help: help,
inribbon: false,
fluent: true
});
},
destroy: function(me)
{
Ext.tip.QuickTipManager.unregister(me.getId());
}
}
});
}
let itemConfig = {
xtype: 'container',
layout: {
type: 'hbox',
align: 'stretch'
},
cls: 'a-column-header-inner',
minWidth: Ametys.form.ConfigurableFormPanel.FIELD_MINWIDTH + Ametys.form.ConfigurableFormPanel.Repeater.FIELD_MARGIN_RIGHT,
items: innerItems
};
let widgetParams = item['widget-params'];
if (widgetParams && widgetParams.width)
{
Ext.apply(itemConfig, {
width: Number(widgetParams.width) + Ametys.form.ConfigurableFormPanel.Repeater.FIELD_MARGIN_RIGHT
});
if (widgetParams.minWidth) {
Ext.apply(itemConfig, {
minWidth: Number(widgetParams.minWidth) + Ametys.form.ConfigurableFormPanel.Repeater.FIELD_MARGIN_RIGHT
});
}
}
else
{
Ext.apply(itemConfig, {
flex: widgetParams && widgetParams.flex ? parseFloat(widgetParams.flex) : 1
});
}
items.push(itemConfig);
}
return items;
},
/**
* Clear all the repeater items
*/
reset: function()
{
var items = this.getItems();
for (var i = items.getCount() - 1; i >= 0; i--)
{
this.removeItem(items.getAt(i));
}
},
/**
* Get the miminum size ie. the miminum amount of entries
* @return {Number} The miminum size of the repeater
*/
getMinSize: function ()
{
return this.minSize;
},
/**
* Add a new repeater item.
* @param {Object} options The options.
* @param {Number} options.position The index at which the Component will be inserted into the Container's items collection. The position is 0-based. Can be null to add at the end.
* @param {Number} options.previousPosition The panel previous position.
* @param {Boolean} [options.collapsed=true] Whether to render the panel collapsed or not.
* @param {Boolean} [options.animate=true] `true` to animate the panel, `false` otherwise.
* @return The panel corresponding to the new instance
*/
createRepeaterItemPanel: function (options)
{
var opt = options || {};
var pos = Ext.isNumber(opt.position) ? opt.position : this.getItemCount();
this._lastInsertItemPosition = pos + 1;
var children = [{
xtype: 'numberfield',
name: '_' + this.prefix + this._getNameAtIndex(pos + 1) + this.defaultPathSeparator + 'previous-position',
value: Ext.isNumber(opt.previousPosition) ? opt.previousPosition + 1 : -1,
hidden: true
}];
if (this.mode == 'table' && !this.readOnly)
{
// Add the tools in the fist column
children.push({
xtype: 'container',
cls: 'a-container-tool',
width: Ametys.form.ConfigurableFormPanel.Repeater.TOOLS_COLUMN_WIDTH,
layout: {
type: 'hbox',
align: 'stretch'
},
items: [
this._deleteTool(this.delLabel),
this._addTool(this.addLabel),
this._upTool(),
this._downTool()
]
});
}
var item = Ext.create('Ext.Panel', {
// title: this.label + ' (' + (pos+1) + ')',
minTitle: this.label,
index: pos, // Index in the item panels list, 0-based.
ui: 'light',
border: true,
cls: 'a-repeater-item a-repeater-item-level-' + this.nestingLevel + (this.nestingLevel % 2 == 0 ? ' even' : ' odd'),
anchor: '100%',
_externalDisableConditionsValues: this._newItemExternalDisableConditionsValues,
items: children,
listeners: {
add: {fn: this._onAddComponent, scope: this}
}
});
if (this.mode == 'table')
{
Ext.apply(item, {
layout: {
type: 'hbox',
align: 'stretch'
},
header: false,
defaults: {
flex: 1
}
});
}
else
{
// The tools
var tools = this.readOnly ? null : [
// collapse tool (automatically added)
this._upTool(),
this._downTool(),
this._addTool(this.addLabel),
this._deleteTool(this.delLabel)
];
Ext.apply(item, {
bodyPadding: Ametys.form.ConfigurableFormPanel.Repeater.PADDING + ' ' + Ametys.form.ConfigurableFormPanel.Repeater.PADDING + ' 0 ' + Ametys.form.ConfigurableFormPanel.Repeater.PADDING,
header: {
titlePosition: 1
},
tools: tools,
layout: {
type: 'anchor'
},
titleCollapse: true,
hideCollapseTool: false,
collapsible: true,
collapsed: opt.collapsed !== false // Render the items collapsed by default
});
}
if (Ext.isNumber(opt.position))
{
var items = this.getItems();
// Shift the items after the insert position.
for (var i = items.getCount() - 1; i >= opt.position; i--)
{
var itemPanel = items.getAt(i);
this._increaseIndexOfFields(itemPanel.index + 1);
itemPanel.index = itemPanel.index + 1;
this._updatePanelHeader(itemPanel);
}
// Position is 0 -> insert at position #1 to insert after the size field.
this.insert(opt.position + 1, item);
}
else
{
this.add(item);
}
this._updatePanelHeader(item);
this.incrementItemCount();
this._updateGlobalHeader(true);
return item;
},
/**
* Draw the repeater fields
* @param {Ext.Panel} panel The item panel.
* @param {Object} options Options.
*/
drawRepeaterItemFields: function(panel, options)
{
var opt = options || {};
var index = panel.index + 1;
// Transmit offset + 20 (margin) + 5 (padding) + 1 (border).
var offset = this.offset
+ Ametys.form.ConfigurableFormPanel.Repeater.PADDING
+ 1 + (this.nestingLevel > 1 ? Ametys.form.ConfigurableFormPanel.Repeater.NESTED_OFFSET : 0);
var roffset = this.roffset
+ Ametys.form.ConfigurableFormPanel.Repeater.PADDING
+ 1;
// Draw the fields.
// Hide labels for each fields if mode == table
if (this.mode == 'table')
{
let data = this.composition;
for (let name in data)
{
if (!data[name])
{
continue;
}
let item = data[name];
item.hideLabel = true;
item.hideDescription = true;
item.style = 'margin-right: ' + Ametys.form.ConfigurableFormPanel.Repeater.FIELD_MARGIN_RIGHT + 'px';
item.minWidth = Ametys.form.ConfigurableFormPanel.FIELD_MINWIDTH;
item.repeaterMode = this.mode;
}
}
Ext.defer(this.form._configureJSON, 0, this.form, [this.composition, this.prefix + this._getNameAtIndex(index) + this.defaultPathSeparator, panel, offset, roffset]);
if (this.fieldCheckers)
{
this.form._fieldCheckersManager.addFieldCheckers(this.items.get(index), this.fieldCheckers, this.prefix + this._getNameAtIndex(index) + this.defaultPathSeparator, offset, roffset);
}
// Default to true
// panel.expand(opt.animate !== false);
},
/**
* Add a new repeater item.
* @param {Object} options The item panel.
* @param {Boolean} [scrollTo=false] When true, the form will scroll to this new element
* @return {Ext.panel.Panel} The newly created repeater item panel.
*/
addRepeaterItem: function(options, scrollTo) // function(position)
{
// Suspend layout update.
Ext.suspendLayouts();
try
{
var options = options || {};
if (options.fireRepeaterEntryReadyEvent)
{
this.form.notifyAddRepeaterEntry(true);
}
// Create the item panel.
var itemPanel = this.createRepeaterItemPanel(options);
// Draw the item fields.
this.drawRepeaterItemFields(itemPanel, options);
this._updateGlobalHeader(true);
// Update the tools visibility.
if (itemPanel.rendered)
{
this._updateToolsVisibility();
if (scrollTo)
{
this._scrollToNewPanel(itemPanel);
}
}
else
{
itemPanel.on('render', this._updateToolsVisibility, this);
if (scrollTo)
{
itemPanel.on('boxready', this._scrollToNewPanel, this);
}
}
}
finally
{
// Resume layout update and force to recalculate the layout.
Ext.resumeLayouts(true);
}
if (options.fireRepeaterEntryReadyEvent)
{
this.form.notifyAddRepeaterEntry(false);
this.form.fireEvent('repeaterEntryReady', this);
}
return itemPanel;
},
/**
* @private
* Scroll the form to make the new panel visible
* @param {Ext.panel.Panel} panel The new panel to scroll to
*/
_scrollToNewPanel: function(panel)
{
var newHeight = panel.getHeight();
var newTop = panel.getPosition()[1]; // get the bottom of the previous
var visibleTop = this.form.getPosition()[1];
var visibleSize = this.form.getHeight();
var newPos = Math.min((newTop + newHeight) - (visibleTop + visibleSize) + 10, newTop-visibleTop);
if (newPos > 0)
{
this.form.scrollBy(0, newPos);
}
},
/**
* Remove a repeater entry.
* @param {Ext.panel.Panel} itemPanel The item panel to remove.
*/
removeItem: function(itemPanel)
{
// Position of the panel to delete
var position = itemPanel.index;
// Remove the repeater item.
this.remove(itemPanel);
this._removeFields(position + 1);
// Get the next panel in repeater (the panel has been already removed => position + 1).
var itemPanel = this.items.getAt(position + 1);
while (itemPanel != null)
{
// Update the repeater fields index.
this._decreaseIndexOfFields(itemPanel.index + 1);
itemPanel.index = itemPanel.index - 1;
this._updatePanelHeader(itemPanel);
itemPanel = itemPanel.nextSibling();
}
// Decrease the repeater size.
this.decrementItemCount();
this._updateToolsVisibility();
// Show the repeater header if there is no more entry.
if (this.getItemCount() < 1)
{
this._updateGlobalHeader(false);
}
},
/**
* Get the repeater items.
* @return {Ext.util.MixedCollection} the repeater items.
*/
getItems: function()
{
// Filter the hidden panels.
return this.items.filterBy(function(panel, key) {
return !panel.isHidden();
});
},
/**
* Get the repeater item count.
* @return {Number} the repeater item count.
*/
getItemCount: function()
{
// Remove 1 for the hidden count field.
return this.items.getCount() - 1;
},
/**
* Get the repeater's label
* @return {String} the label
*/
getLabel: function ()
{
return this.label;
},
/**
* Returns whether or not the repeater is currently valid
* @return True if the repeater is valid, else false
*/
isValid: function ()
{
var errors = this.getErrors(),
isValid = Ext.isEmpty(errors);
if (isValid)
{
this.clearInvalid();
}
else
{
this.markInvalid(errors);
}
return isValid;
},
/**
* Returns whether or not the repeater is currently valid,
* and fires the {@link #validitychange} event if the repeater validity has changed since the last validation.
*
* @return {Boolean} True if the repeater is valid, else false
*/
validate : function()
{
var me = this,
isValid = me.isValid();
if (isValid !== me.wasValid) {
me.wasValid = isValid;
me.fireEvent('validitychange', me, isValid);
}
return isValid;
},
/**
* Runs repeater's validations and returns an array of any errors
* @return {String[]} All validation errors for this field
*/
getErrors: function ()
{
var errors = [];
if (this.minSize != null && this.getItemCount() < this.minSize)
{
errors.push("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT_INVALID_MINSIZE}}: " + this.getItemCount() + '/' + this.minSize);
}
if (this.maxSize != null && this.getItemCount() > this.maxSize)
{
errors.push("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT_INVALID_MAXSIZE}}: " + this.getItemCount() + '/' + this.maxSize);
}
return errors;
},
/**
* Display one or more error messages associated with this repeater
* @param {String/String[]} errors The validation message(s) to display.
*/
markInvalid: function (errors)
{
errors = Ext.Array.from(errors);
var me = this,
hasError = !Ext.isEmpty(errors);
if (me.rendered && !me.isDestroyed)
{
var tpl = me.getTpl('activeErrorsTpl');
var activeError = tpl.apply({
errors: errors,
listCls: Ext.plainListCls + ' a-repeater-invalid-tooltip'
});
if (me.getItemCount() > 0)
{
me.getItems().each(function(panel, index, length) {
// Remove old errors if exists
if (panel.tools && panel.tools.error)
{
panel.getHeader().remove(panel.tools.error);
}
if (hasError)
{
me.clearErrorMessages(panel.getHeader() || panel.header);
panel.addTool({type: 'error', qtip: activeError});
}
});
}
else
{
me.clearErrorMessages(me.getHeader() || me.header);
me.addTool({type: 'error', qtip: activeError});
}
}
this.el[hasError ? 'addCls' : 'removeCls'](this.invalidCls);
},
/**
* @private
* Clear any error messages
* @param {Ext.panel.Header} header The panel's header
*/
clearErrorMessages : function (header)
{
var tools = header.tools || [];
var errorTools = header.items.filter('type', 'error');
if (errorTools != null)
{
errorTools.each (function (item) {
Ext.Array.remove(tools, item);
header.remove(item);
});
}
},
/**
* Clear any invalid styles/messages for this repeater.
*/
clearInvalid: function ()
{
var me = this;
me.removeCls(me.invalidCls);
if (me.getItemCount() > 0)
{
me.getItems().each(function(panel, index, length) {
if (panel.tools && panel.tools.error)
{
panel.getHeader().remove(panel.tools.error);
}
});
}
else
{
if (me.getHeader().tools && me.getHeader().tools.error)
{
me.getHeader().remove(me.getHeader().tools.error);
}
}
},
/**
* Expand all the repeater items.
*/
expandAll: function()
{
if (this.mode !== 'table')
{
Ext.suspendLayouts();
var me = this;
me.getItems().each(function(panel, index, length) {
if (panel.rendered)
{
panel.expand();
}
else
{
panel.collapsed = false;
}
});
Ext.resumeLayouts(true);
}
},
/**
* Collapse all the repeater items.
*/
collapseAll: function()
{
if (this.mode !== 'table')
{
Ext.suspendLayouts();
var me = this;
me.getItems().each(function(panel, index, length) {
if (panel.rendered && panel.isVisible(true))
{
panel.collapse();
}
else
{
panel.collapsed = true;
}
});
Ext.resumeLayouts(true);
}
},
/**
* @private
* Get the field that holds the size of the repeater
* @return {Ext.form.field.Field} The field
*/
getSizeField: function()
{
return this.items.first().items.first();
},
/**
* @private
* Increment the value of the internal field holding the repeater size
*/
incrementItemCount: function()
{
var sizeField = this.getSizeField();
sizeField.setValue(sizeField.getValue() + 1);
},
/**
* @private
* Decrement the value of the internal field holding the repeater size
*/
decrementItemCount: function()
{
var sizeField = this.getSizeField();
sizeField.setValue(Math.max(sizeField.getValue() - 1, 0));
},
/**
* Set an item's previous position field.
* @param {Number} position the item's position in the repeater, 0-based.
* @param {Number} previousPosition the item's previous position.
*/
setItemPreviousPosition: function(position, previousPosition)
{
// Get the corresponding item.
var item = this.getItems().getAt(position);
// The item panel's first element is the previous position field.
item.items.first().setValue(previousPosition + 1);
},
/**
* Mark an item as new by modifying its previous position field.
* @param {Number} position the item's position in the repeater, 0-based.
*/
markItemAsNew: function(position)
{
// Get the corresponding item.
var item = this.getItems().getAt(position);
// The item panel's first element is the previous position field.
item.items.first().setValue(-1);
},
/**
* Set the external disable conditions values of the item at given position
* @param {Number} position the item's position in the repeater, 0-based.
* @param {Object} values the external disable conditions values to set
*/
setItemExternalDisableConditionsValues: function(position, values)
{
// Get the corresponding item.
var item = this.getItems().getAt(position);
// Set the conditions values
item._externalDisableConditionsValues = values;
},
/**
* Retrieves the item's external disable conditions values
* @param {Number} position the item's position in the repeater, 0-based.
*/
getItemExternalDisableConditionsValues: function(position)
{
// Get the corresponding item.
var item = this.getItems().getAt(position);
// Set the conditions values
return item._externalDisableConditionsValues;
},
/**
* Set the external disable conditions values for the new items of the repeater
* @param {Object} values the external disable conditions values to set
*/
setNewItemExternalDisableConditionsValues: function(values)
{
this._newItemExternalDisableConditionsValues = values;
},
/**
* Hide or show the tools
* @private
*/
_updateToolsVisibility: function()
{
if (!this.readOnly)
{
var me = this;
var items = me.getItems();
// Iterate on repeater item panels.
items.each(function(panel, index, length) {
me._setVisible("moveup", index > 0, panel);
me._setVisible("movedown", index < (length-1), panel);
me._setVisible("plus", me.maxSize == null || length < me.maxSize, panel);
me._setVisible("delete", me.minSize == null || length > me.minSize, panel);
});
// Display global header "plus" button only if the max size is not reached
window.setTimeout(function(){me._setVisible("plus", me.maxSize == null || items.length < me.maxSize, me)}, 1);
}
},
/**
* Update visibility of a tool
* @param {String} elementName the name of element to set visible or not.
* @param {Boolean} visible true to set the element visible.
* @param {Ext.panel.Panel} panel the panel containing the element.
* @private
*/
_setVisible: function(elementName, visible, panel)
{
if (this.mode == 'table')
{
// In mode table, only set tools as disable, do not hide them
panel.down("[type='" + elementName + "']").setDisabled(!visible);
}
else
{
// The panel is rendered: tools is an object, items can be accessed by their name.
if (panel.tools[elementName])
{
panel.tools[elementName].setVisible(visible);
}
// The panel is not rendered yet: tools is a configuration array.
else
{
Ext.Array.findBy(panel.tools, function(f) { return f.type == elementName; }).hidden = !visible;
}
}
},
/**
* Decrease the index of repeater fields
* @param {Number} index The start index
* @private
*/
_decreaseIndexOfFields: function (index)
{
this._shiftIndexOfFields(index, -1);
},
/**
* Increase the index of repeater fields
* @param {Number} index The start index
* @private
*/
_increaseIndexOfFields: function (index)
{
this._shiftIndexOfFields(index, 1);
},
/**
* Shift the index of repeater fields
* @param {Number} index The start index
* @param {Number} offset The offset to shift
* @private
*/
_shiftIndexOfFields: function (index, offset)
{
var me = this;
var fieldsToRename = {};
// Shift standard fields.
var prefix = me.prefix + me._getNameAtIndex(index);
var fieldNames = me.form.getFieldNames();
for (var i = 0; i < fieldNames.length; i++)
{
var fieldName = fieldNames[i];
if (fieldName.indexOf(prefix + me.defaultPathSeparator) == 0)
{
var field = me.form.getField(fieldName);
var newName = me.prefix + me._getNameAtIndex(index + offset) + me.defaultPathSeparator + fieldName.substring(prefix.length + 1);
me._setFieldName(field, newName);
fieldsToRename[fieldName] = newName;
}
}
// Shift subrepeaters recursiverly
for (let subrepeater of this.form.getRepeaters(this.id))
{
if (subrepeater.prefix.indexOf(prefix + me.defaultPathSeparator) == 0)
{
subrepeater.prefix = me.prefix + me._getNameAtIndex(index + offset) + subrepeater.prefix.substring(prefix.length);
}
}
// Shift hidden fields (which name starts with an underscore).
prefix = '_' + me.prefix + me._getNameAtIndex(index);
me.form.getForm().getFields().each(function(formField) {
if (formField.name && formField.name.indexOf(prefix + me.defaultPathSeparator) == 0)
{
var newName = '_' + me.prefix + me._getNameAtIndex(index + offset) + me.defaultPathSeparator + formField.name.substring(prefix.length + 1);
me._setFieldName(formField, newName);
}
});
for (var oldName in fieldsToRename)
{
me.form._onRenameField(oldName, fieldsToRename[oldName]);
}
},
/**
* Switch index of two repeater fields
* @param {Number} index1 Index of first field
* @param {Number} index2 Index of second field
* @private
*/
_switchIndexOfFields: function(index1, index2)
{
var me = this;
var fieldsToRename = [];
// Switch standard fields.
var prefix1 = me.prefix + me._getNameAtIndex(index1);
var prefix2 = me.prefix + me._getNameAtIndex(index2);
var fieldNames = me.form.getFieldNames();
for (var i = 0; i < fieldNames.length; i++)
{
var fieldName = fieldNames[i];
if (fieldName.indexOf(prefix1 + me.defaultPathSeparator) == 0)
{
var field = me.form.getField(fieldName);
var newName = me.prefix + me._getNameAtIndex(index2) + me.defaultPathSeparator + fieldName.substring(prefix1.length + 1);
fieldsToRename.push({index: i, field: field, newName: newName});
}
else if (fieldName.indexOf(prefix2 + me.defaultPathSeparator) == 0)
{
var field = me.form.getField(fieldName);
var newName = me.prefix + this._getNameAtIndex(index1) + me.defaultPathSeparator + fieldName.substring(prefix2.length + 1);
fieldsToRename.push({index: i, field: field, newName: newName});
}
}
// Switch subrepeaters recursiverly
for (let subrepeater of this.form.getRepeaters(this.id))
{
if (subrepeater.prefix.indexOf(prefix1 + me.defaultPathSeparator) == 0)
{
subrepeater.prefix = prefix2 + subrepeater.prefix.substring(prefix1.length);
}
else if (subrepeater.prefix.indexOf(prefix2 + me.defaultPathSeparator) == 0)
{
subrepeater.prefix = prefix1 + subrepeater.prefix.substring(prefix2.length);
}
}
// Switch hidden fields (which name starts with an underscore).
prefix1 = '_' + me.prefix + me._getNameAtIndex(index1);
prefix2 = '_' + me.prefix + me._getNameAtIndex(index2);
me.form.getForm().getFields().each(function(formField) {
if (formField.name && formField.name.indexOf(prefix1 + me.defaultPathSeparator) == 0)
{
var newName = '_' + me.prefix + me._getNameAtIndex(index2) + me.defaultPathSeparator + formField.name.substring(prefix1.length + 1);
me._setFieldName(formField, newName);
}
else if (formField.name && formField.name.indexOf(prefix2 + me.defaultPathSeparator) == 0)
{
var newName = '_' + me.prefix + me._getNameAtIndex(index1) + me.defaultPathSeparator + formField.name.substring(prefix2.length + 1);
me._setFieldName(formField, newName);
}
});
for (var i = 0; i < fieldsToRename.length; i++)
{
me._setFieldName(fieldsToRename[i].field, fieldsToRename[i].newName);
me.form._onRenameField(fieldsToRename[i].index, fieldsToRename[i].newName);
}
},
/**
* Remove the references to field names in the form.
* @param {Number} index The index of the removed form item.
* @private
*/
_removeFields: function(index)
{
var fieldNames = this.form.getFieldNames();
var fieldsToRemove = [];
var prefix = this.prefix + this._getNameAtIndex(index) + this.defaultPathSeparator;
for (var i = 0; i < fieldNames.length; i++)
{
if (fieldNames[i].indexOf(prefix) == 0)
{
fieldsToRemove.push(fieldNames[i]);
}
}
for (var i = 0; i < fieldsToRemove.length; i++)
{
this.form._onRemoveField(fieldsToRemove[i]);
}
},
/**
* @private
* Change a field name. Used when position of a repeater line has changed.
* @param {Ext.form.field.Field} field The field to rename
* @param {String} newName The new name of the field
*/
_setFieldName: function(field, newName)
{
field.name = newName;
if (typeof field.setName == 'function')
{
field.setName(newName);
}
var input = Ext.get(field.getInputId());
if (input != null)
{
input.dom.name = newName;
}
},
/**
* Called when the owner form is ready, all its fields initialized and valued.
* @param {Ametys.form.ConfigurableFormPanel} form The owner form.
* @private
*/
_onFormReady: function(form)
{
this._updateAllItemHeaders();
},
/**
* Called when the an repeater entry has been added and is ready (all its fields initialized)
* @param {Ametys.form.ConfigurableFormPanel.Repeater} repeater The repeater containing the entry.
* @private
*/
_onRepeaterEntryReady: function(repeater)
{
// updates only
if (repeater === this)
{
this._updateAllItemHeaders();
}
},
/**
* Called when a component is added to an item panel.
* @param {Ext.panel.Panel} panel The container panel.
* @param {Ext.Component} component The added component.
* @param {Number} index The component index.
* @private
*/
_onAddComponent: function(panel, component, index)
{
// When a specific header label is specified, monitor when field values change.
if (component.isFormField && this.headerLabel)
{
// Monitor only metadata the template is based on.
var shortName = component.shortName;
if (shortName && this._headerFields.indexOf(shortName) >= 0)
{
// When the field loses focus, update the header of its entry.
component.on('change', Ext.bind(this._updatePanelHeader, this, [panel]), this);
}
}
component.on('resize', function(elt, w, h, oldW, oldH) {
if (oldH && oldH != h) // non-empty oldH means that is not the first size ; moreover the issue happens only in vertical resize
{
// When a subcomponent of this repeater is resized we need to enlarge (not scroll)
panel.updateLayout();
}
})
},
/**
* Update all item panel headers from their fields.
* @private
*/
_updateAllItemHeaders: function()
{
var me = this;
me.getItems().each(function(panel, index, length) {
me._updatePanelHeader(panel);
});
},
/**
* Update a panel header from its fields.
* @param {Ext.panel.Panel} panel The panel which header to update.
* @private
*/
_updatePanelHeader: function(panel)
{
let addTitle = "";
if (this.headerLabel)
{
var subFields = panel.query('> *[shortName]');
var emptyValue = true;
// Iterate over all the fields.
var values = {};
for (var i = 0; i < subFields.length; i++)
{
// Process only the fields which are used in the header template.
var shortName = subFields[i].shortName;
if (this._headerFields.indexOf(shortName) >= 0)
{
// Get the value and test if it's empty.
var value = Ext.String.escapeHtml(subFields[i].getReadableValue());
values[shortName] = value;
if (value != null && value != '')
{
emptyValue = false;
}
}
}
addTitle = this._headerTpl.apply(values);
}
// Compute and set the new panel header/title.
var newTitle = panel.minTitle + ' (' + (panel.index+1) + ')';
if (!emptyValue && addTitle)
{
newTitle = newTitle + '<span class="header-repeater-item-value"> - ' + addTitle + '</span>';
}
panel.setTitle(newTitle);
},
/**
* Update the global header.
* @param {Boolean} hasEntry True if the repeater has at least one entry
* @private
*/
_updateGlobalHeader: function(hasEntry)
{
if (this.mode !== 'table')
{
if (hasEntry)
{
this._toolExpandAll.show();
this._toolCollapseAll.show();
}
else
{
this._toolExpandAll.hide();
this._toolCollapseAll.hide();
}
}
var label = this.label + (hasEntry ? '' : ' (0)');
this.getHeader().setTitle ? this.getHeader().setTitle(label) : this.getHeader().title = label;
},
// ----------------------------------------------------
/**
* Creates the general 'add' tool with the given label
* @private
*/
_addFirst: function (label)
{
return {
xtype: 'tool',
type: 'plus',
qtip: label,
handler: this._add,
scope: this
};
},
/**
* Creates the 'add' tool with the given label
* @private
*/
_addTool: function(label)
{
return {
xtype: 'tool',
type: 'plus',
qtip: label,
handler: this._insert,
scope: this
};
},
/**
* Creates the 'delete' tool with the given label
* @private
*/
_deleteTool: function(label)
{
return {
xtype: 'tool',
type: 'delete',
qtip: label,
handler: function(event, toolEl, header, tool) {
// header.ownerCt returns the item panel.
this._delete(header.ownerCt);
},
scope: this
};
},
/**
* Creates the 'move down' tool with the given label
* @private
*/
_downTool: function()
{
return {
xtype: 'tool',
type: 'movedown',
qtip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_MOVE_DOWN}}",
handler: this._down,
scope: this
};
},
/**
* Creates the 'move up' tool with the given label
* @private
*/
_upTool: function()
{
return {
xtype: 'tool',
type: 'moveup',
qtip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_MOVE_UP}}",
handler: this._up,
scope: this
};
},
// ----------------------------------------------------
// Tool actions
/**
* Add a new repeater instance at the end of the list.
* @param {Ext.event.Event} event The click event.
* @param {Ext.Element} toolEl The tool Element.
* @param {Ext.panel.Header} header The host panel header.
* @param {Ext.panel.Tool} tool The tool object
*/
_add: function(event, toolEl, header, tool)
{
if (this.maxSize != null && this.getItemCount() >= this.maxSize)
{
Ametys.Msg.show({
title: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT}}",
msg: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT_ERROR_MAXSIZE}}",
buttons: Ext.Msg.OK,
icon: Ext.MessageBox.ERROR
});
return;
}
this.addRepeaterItem({position: 0, collapsed: false, fireRepeaterEntryReadyEvent: true});
this.validate();
},
/**
* Insert a new repeater instance after the given panel.
* @param {Ext.event.Event} event The click event.
* @param {Ext.Element} toolEl The tool Element.
* @param {Ext.panel.Header} header The host panel header.
* @param {Ext.panel.Tool} tool The tool object
*/
_insert: function(event, toolEl, header, tool)
{
if (this.maxSize != null && this.getItemCount() >= this.maxSize)
{
Ametys.Msg.show({
title: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT}}",
msg: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT_ERROR_MAXSIZE}}",
buttons: Ext.Msg.OK,
icon: Ext.MessageBox.ERROR
});
return;
}
var panel = header.ownerCt;
this.addRepeaterItem({position: panel.index + 1, collapsed: false, fireRepeaterEntryReadyEvent: true}, true);
this.validate();
},
/**
* Move down the given panel in its repeater
* @param {Ext.event.Event} event The click event.
* @param {Ext.Element} toolEl The tool Element.
* @param {Ext.panel.Header} header The host panel header.
* @param {Ext.panel.Tool} tool The tool object
*/
_down: function(event, toolEl, header, tool)
{
var itemPanel = header.ownerCt;
var index = itemPanel.index + 1;
if (index >= this.getItemCount())
{
return;
}
var items = this.getItems();
var itemPanel2 = items.getAt(itemPanel.index + 1);
this.move(index, index + 1);
// Switch all fields. Position fields will be updated as well.
this._switchIndexOfFields(index, index + 1);
var tmpIndex = itemPanel.index;
itemPanel.index = itemPanel2.index;
itemPanel2.index = tmpIndex;
// Update title (that depends on position, but also on fields)
this._updatePanelHeader(itemPanel);
this._updatePanelHeader(itemPanel2);
itemPanel.expand();
// Update tools
this._updateToolsVisibility();
},
/**
* Move up the given panel in its repeater
* @param {Ext.event.Event} event The click event.
* @param {Ext.Element} toolEl The tool Element.
* @param {Ext.panel.Header} header The host panel header.
* @param {Ext.panel.Tool} tool The tool object
*/
_up: function(event, toolEl, header, tool)
{
var itemPanel = header.ownerCt;
var index = itemPanel.index + 1;
if (index <= 0)
{
return;
}
var items = this.getItems();
var itemPanel2 = items.getAt(itemPanel.index - 1);
this.move(index, index - 1);
// Switch all fields. Position fields will be updated as well.
this._switchIndexOfFields(index, index - 1);
var tmpIndex = itemPanel.index;
itemPanel.index = itemPanel2.index;
itemPanel2.index = tmpIndex;
// Update title (that depends on position, but also on fields)
this._updatePanelHeader(itemPanel);
this._updatePanelHeader(itemPanel2);
itemPanel.expand();
// Update tools
this._updateToolsVisibility();
},
/**
* @private
* Removes a repeater entry.
* @param {Ext.Panel} itemPanel The panel to delete
*/
_delete: function(itemPanel)
{
if (this.minSize != null && this.getItemCount() <= this.minSize)
{
Ametys.Msg.show({
title: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE}}",
msg: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE_ERROR_MINSIZE}}",
buttons: Ext.Msg.OK,
icon: Ext.MessageBox.ERROR
});
return;
}
// Confirm deletion
Ametys.Msg.confirm(
"{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE}}",
"{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE_CONFIRM}}",
function (answer)
{
if (answer == 'yes')
{
// Remove the entry.
this.removeItem(itemPanel);
this.validate();
}
},
this
);
},
/**
* Retrieves the name of the repeater at the given index
* @param {Number} index The index
* @return {String} The name of the repeater at the given index
* @private
*/
_getNameAtIndex: function(index)
{
return this.self.getNameAtIndex(this.name, this.defaultPathSeparator, index);
},
enable: function()
{
let me = this;
this.callParent(arguments);
if (this.mode !== 'table')
{
this.tools.forEach(function(tool) { if (tool.show) { tool.show() } else { tool.hidden = false; } });
this.expand();
}
this._updateGlobalHeader(this.getItemCount() > 0);
},
disable: function()
{
this.callParent(arguments);
if (this.mode !== 'table')
{
this.tools.forEach(function(tool) { if (tool.hide) { tool.hide() } else { tool.hidden = true; } });
this.collapse();
}
}
});
/**
* Repeater-specific accordion layout, which allows to collapse all items.
* @private
*/
Ext.define('Ametys.cms.form.layout.Repeater', {
extend: 'Ext.layout.container.Accordion',
alias: ['layout.repeater-accordion'],
/**
* Overridden to prevent automatically expanding an item when another is collapsed.
*/
onComponentCollapse: function(comp)
{
// Do nothing.
}
/*onContentChange: function () {
this.owner.updateLayout({isRoot: true});
return true;
}*/
});