/*
* 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.
*/
/**
* Provides an advanced widget for dates, which are adaptable to the instant they are resolved (they can be relative to this instant)
* <br>Be careful, the current version of this widget returns a String-type value !
*/
Ext.define('Ametys.form.widget.AdaptableDate', {
extend: 'Ametys.form.AbstractFieldsWrapper',
statics: {
/**
* @property {Array[]} __UNITS_STORE_CONFIG The configuration for combobox of temporal units (the values must comply to the Java 'java.time.temporal.ChronoUnit' units)
* @private
* @readonly
*/
__UNITS_STORE_CONFIG: [
['DAYS', "{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_UNIT_DAYS}}"],
['WEEKS', "{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_UNIT_WEEKS}}"],
['MONTHS', "{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_UNIT_MONTHS}}"],
['YEARS', "{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_UNIT_YEARS}}"]
],
/**
* @property {Number} __NUMBER_FIELD_WIDTH The width for number fields
* @private
* @readonly
*/
__NUMBER_FIELD_WIDTH: 60
},
/**
* @cfg {String} nowOptionText The text for the 'NOW' option
*/
nowOptionText: "{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_NOW_OPTION_DEFAULT_TEXT}}",
/**
* @cfg {String} emptyText The text for the empty field
*/
emptyText: "{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_EMPTY_TEXT}}",
/**
* @private {Number} _suspendUiUpdating Internal counter to know if radios and subfields must be updated (when == 0) when calling {@link #setValue}
*/
_suspendUiUpdating: 0,
initComponent: function()
{
var menu = Ext.create('Ext.menu.Menu', {
items: [{
xtype: 'container',
margin: '0 10 10 10', // top right bottom left
layout: {
type: 'vbox',
align: 'stretch'
},
itemId: 'radiocontainer',
items: this._getRadioContainerItems([
this._nowOption(),
this._pastOption(),
this._futureOption(),
this._staticDateOption()
])
}]
});
this.items = [{
xtype: 'component',
itemId: 'readableValue',
cls: Ametys.form.AbstractField.READABLE_TEXT_CLS,
html: null
}, {
xtype: 'button',
iconCls: 'ametysicon-datetime-calendar-day',
menu: menu
}];
if (this.allowBlank !== false)
{
this.items.push({
xtype: 'button',
iconCls: 'ametysicon-sign-raw-cross',
handler: function(btn)
{
this.down('#radiocontainer').items.each(function(lineContainer) {
var radio = this._retrieveRadio(lineContainer);
radio.setValue(false);
}, this);
this._setInternalValue(null);
},
scope: this
});
}
this.defaults = {
style: {
marginRight: '4px'
}
};
this.callParent(arguments);
},
afterRender: function()
{
this.callParent(arguments);
this._updateUI();
},
/**
* @private
* Gets the configuration of items for the 'radio container'
* @param {Object[]} configArrays Arrays of component configurations which must be placed on the same line
* @return {Object[]} the configuration of items for the 'radio container'
*/
_getRadioContainerItems: function(configArrays)
{
return Ext.Array.map(configArrays, function(configArray) {
var items = Ext.Array.filter(configArray, function(cfg) { return cfg != null; });
return {
xtype: 'container',
layout: {
type: 'hbox',
align: 'middle'
},
defaults: {
style: {
marginRight: '6px',
marginBottom: '10px'
}
},
items: items
};
});
},
/**
* @private
* Gets the configuration of items for the line 'NOW/TODAY/CURRENT...'
* @return {Object[]} the configuration of items for the line 'NOW/TODAY/CURRENT...'
*/
_nowOption: function()
{
return [{
xtype: 'radio',
name: 'type',
itemId: 'now-radio',
inputValue: 'now',
boxLabel: this.nowOptionText,
listeners: {
'change': this._onRadioChange,
scope: this
}
}];
},
/**
* @private
* Gets the configuration of items for the line '4 days/weeks/months ago' (past)
* @return {Object[]} the configuration of items for the line '4 days/weeks/months ago' (past)
*/
_pastOption: function()
{
var text1 = this._textCmpCfg("{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_PAST_OPTION_TEXT_1}}"),
number = this._numberFieldCfg('past-value'),
text2 = this._textCmpCfg("{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_PAST_OPTION_TEXT_2}}"),
unit = this._unitComboboxCfg('past-unit'),
text3 = this._textCmpCfg("{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_PAST_OPTION_TEXT_3}}");
return [{
xtype: 'radio',
name: 'type',
itemId: 'past-radio',
inputValue: 'past',
listeners: {
'change': this._onRadioChange,
scope: this
}
},
text1,
number,
text2,
unit,
text3
];
},
/**
* @private
* Gets the configuration of items for the line 'in 4 days/weeks/months' (future)
* @return {Object[]} the configuration of items for the line 'in 4 days/weeks/months' (future)
*/
_futureOption: function()
{
var text1 = this._textCmpCfg("{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_FUTURE_OPTION_TEXT_1}}"),
number = this._numberFieldCfg('future-value'),
text2 = this._textCmpCfg("{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_FUTURE_OPTION_TEXT_2}}"),
unit = this._unitComboboxCfg('future-unit'),
text3 = this._textCmpCfg("{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_FUTURE_OPTION_TEXT_3}}");
return [{
xtype: 'radio',
name: 'type',
itemId: 'future-radio',
inputValue: 'future',
listeners: {
'change': this._onRadioChange,
scope: this
}
},
text1,
number,
text2,
unit,
text3
];
},
/**
* @private
* Gets the configuration of items for the line 'The 01/01/2019' (for selecting a 'static' date)
* @return {Object[]} the configuration of items for the line 'The 01/01/2019' (for selecting a 'static' date)
*/
_staticDateOption: function()
{
var text = this._textCmpCfg("{{i18n PLUGINS_CORE_UI_WIDGET_ADAPTABLE_DATE_STATIC_OPTION_TEXT}}");
return [{
xtype: 'radio',
name: 'type',
itemId: 'staticDate-radio',
inputValue: 'staticDate',
listeners: {
'change': this._onRadioChange,
scope: this
}
},
text,
{
xtype: 'edition.date',
itemId: 'staticDate-value',
allowBlank: false,
value: new Date(),
listeners: {
'focus': this._onFocus,
'change': this._onSubfieldValueChange,
scope: this
}
}];
},
/**
* @private
* Gets the configuration of a text component
* @param {Object/String} baseConfig The base configuration, or just its text as a string
* @return {Object} The configuration object
*/
_textCmpCfg: function(baseConfig)
{
if (Ext.isString(baseConfig))
{
baseConfig = {
html: baseConfig
};
}
if (Ext.isEmpty(baseConfig.html))
{
// no need to draw a useless component
return null;
}
return Ext.apply({
xtype: 'component',
listeners: {
'afterrender': function(cmp) {
cmp.getEl().on('mousedown', Ext.bind(this._onFocus, this, [cmp], 0));
},
scope: this
}
}, baseConfig);
},
/**
* @private
* Gets the configuration of a number field
* @param {String} itemId The item id
* @return {Object} The configuration object
*/
_numberFieldCfg: function(itemId)
{
var minValue = 1;
return {
xtype: 'numberfield',
itemId: itemId,
minValue: minValue,
value: minValue,
allowBlank: false,
allowDecimals: false,
listeners: {
'focus': this._onFocus,
'change': this._onSubfieldValueChange,
scope: this
},
width: this.statics().__NUMBER_FIELD_WIDTH
};
},
/**
* @private
* Gets the configuration of a unit combobox
* @param {String} itemId The item id
* @return {Object} The configuration object
*/
_unitComboboxCfg: function(itemId)
{
return {
xtype: 'combobox',
itemId: itemId,
store: this.statics().__UNITS_STORE_CONFIG,
allowBlank: false,
forceSelection: true,
listeners: {
'focus': this._onFocus,
'change': this._onSubfieldValueChange,
'afterrender': function(combo) {
if (combo.getValue() == null)
{
combo.setValue(combo.getStore().getAt(0));
}
},
scope: this
}
};
},
/**
* @private
* Retrieves the radio button of the given line
* @param {Ext.container.Container} lineContainer The line container
* @return {Ext.form.field.Radio} the radio button of the given line
*/
_retrieveRadio: function(lineContainer)
{
if (lineContainer == null)
{
return null;
}
return lineContainer.items.findBy(function(child) {
return child.getXType() == 'radiofield';
});
},
/**
* @private
* Listener called when a subfield takes the focus, in order to select the associated radio
* @param {Ext.Component} cmp The focused component
*/
_onFocus: function(cmp)
{
var lineContainer = cmp.up(),
radio = this._retrieveRadio(lineContainer);
if (radio && !radio.getValue())
{
radio.setValue(true);
}
},
/**
* @private
* Listener called when a radio is changed, in order to update the internal value
* @param {Ext.form.field.Radio} radio The radio
* @param {Object} newValue The new value
* @param {Object} oldValue The original value
*/
_onRadioChange: function(radio, newValue, oldValue)
{
var lineContainer = radio.up();
if (newValue === true)
{
lineContainer.items.each(function(item) {
if (item.isFormField)
{
item.validate();
}
});
this._setInternalValue(lineContainer);
}
else
{
lineContainer.items.each(function(item) {
if (item.isFormField)
{
item.clearInvalid();
}
});
}
},
/**
* @private
* Listener called when a subfield is changed, in order to update the readable value
* @param {Ext.form.field.Field} subfield The subfield
* @param {Object} newValue The new value
* @param {Object} oldValue The original value
*/
_onSubfieldValueChange: function(subfield, newValue, oldValue)
{
var lineContainer = subfield.up(),
radio = this._retrieveRadio(lineContainer);
if (radio && radio.getValue())
{
this._setInternalValue(lineContainer);
}
},
/**
* @private
* Updates the internal representation of the current value
* @param {Ext.container.Container} selectedLineContainer The selected line container. Can be null for 'empty field'
*/
_setInternalValue: function(selectedLineContainer)
{
this._suspendUiUpdating++;
this.setValue(this._computeStringValue(selectedLineContainer));
this._suspendUiUpdating--;
this._updateReadableValue(selectedLineContainer);
},
setValue: function(value)
{
this.callParent(arguments);
if (this._suspendUiUpdating == 0)
{
// Parse value
var radiocontainer = this.down('#radiocontainer');
if (value == '$today')
{
var radio = radiocontainer.down('#now-radio');
radio.setValue(true);
}
else if (value != null && Ext.String.startsWith(value, '$ago_fullday$'))
{
var amountAndUnit = value.substring('$ago_fullday$'.length),
separatorIndex = amountAndUnit.indexOf('$'),
amount = amountAndUnit.substring(0, separatorIndex),
unit = amountAndUnit.substring(separatorIndex + 1),
radio = radiocontainer.down('#past-radio'),
numberField = radiocontainer.down('#past-value'),
unitField= radiocontainer.down('#past-unit');
radio.setValue(true);
numberField.setValue(amount);
unitField.setValue(unit);
}
else if (value != null && Ext.String.startsWith(value, '$in_fullday$'))
{
var amountAndUnit = value.substring('$in_fullday$'.length),
separatorIndex = amountAndUnit.indexOf('$'),
amount = amountAndUnit.substring(0, separatorIndex),
unit = amountAndUnit.substring(separatorIndex + 1),
radio = radiocontainer.down('#future-radio'),
numberField = radiocontainer.down('#future-value'),
unitField= radiocontainer.down('#future-unit');
radio.setValue(true);
numberField.setValue(amount);
unitField.setValue(unit);
}
else if (value != null)
{
var date = value,
radio = radiocontainer.down('#staticDate-radio'),
dateField = radiocontainer.down('#staticDate-value');
radio.setValue(true);
dateField.setValue(date);
}
}
},
/**
* @private
* Computes the current string value
* @param {Ext.container.Container} selectedLineContainer The selected line container. Can be null for 'empty field'
* @return {String} The value
*/
_computeStringValue: function(selectedLineContainer)
{
var checkedRadio = this._retrieveRadio(selectedLineContainer);
if (checkedRadio)
{
switch (checkedRadio.getSubmitValue()) {
case 'now':
return '$today';
case 'past':
var valueField = selectedLineContainer.getComponent('past-value'),
unitField = selectedLineContainer.getComponent('past-unit'),
value = valueField.isValid() ? valueField.getValue() : null,
unit = unitField.isValid() ? unitField.getValue() : null;
return (value == null || unit == null) ? null : '$ago_fullday$' + value + '$' + unit;
case 'future':
var valueField = selectedLineContainer.getComponent('future-value'),
unitField = selectedLineContainer.getComponent('future-unit'),
value = valueField.isValid() ? valueField.getValue() : null,
unit = unitField.isValid() ? unitField.getValue() : null;
return (value == null || unit == null) ? null : '$in_fullday$' + value + '$' + unit;
case 'staticDate':
var dateField = selectedLineContainer.getComponent('staticDate-value'),
dateStrValue = dateField.isValid() ? dateField.getSubmitValue() : null;
return Ext.isEmpty(dateStrValue) ? null : dateStrValue;
default:
return null;
}
}
else
{
return null;
}
},
/**
* @private
* Updates UI
*/
_updateUI: function()
{
var checkedLineContainer = null;
this.down('#radiocontainer').items.each(function(lineContainer) {
var radio = this._retrieveRadio(lineContainer);
if (radio && radio.getValue())
{
// found, stop iteration
checkedLineContainer = lineContainer;
return false;
}
}, this);
this._updateReadableValue(checkedLineContainer);
},
/**
* @private
* Updates the readable value of this field
* @param {Ext.container.Container} lineContainer The line container. Can be null for 'empty field'
*/
_updateReadableValue: function(lineContainer)
{
var readableValueCmp = this.items.getByKey('readableValue');
if (lineContainer)
{
var val = lineContainer.items.getRange()
.map(function(item) {
var xtype = item.getXType();
return xtype === 'radiofield' && item.boxLabel
|| xtype === 'combobox' && (item.getReadableValue() || '?')
|| xtype === 'numberfield' && (Ext.isNumber(item.getValue()) && item.getValue().toString() || '?')
|| xtype === 'component' && item.getInitialConfig().html
|| xtype === 'edition.date' && (item.getRawValue() || '?')
|| null;
})
.filter(function(txt) { return txt != null && txt.length > 0; })
.map(function(txt) { return txt.trim(); })
.join(' ');
readableValueCmp.setHtml(val);
}
else
{
readableValueCmp.setHtml(this.emptyText);
}
}
});