/**
* Provides a time input field with a time dropdown and automatic time validation.
*
* This field recognizes and uses JavaScript Date objects as its main {@link #value} type
* (only the time portion of the date is used; the month/day/year are ignored). In addition,
* it recognizes string values which are parsed according to the {@link #format} and/or
* {@link #altFormats} configs. These may be reconfigured to use time formats appropriate for
* the user's locale.
*
* The field may be limited to a certain range of times by using the {@link #minValue} and
* {@link #maxValue} configs, and the interval between time options in the dropdown can be changed
* with the {@link #increment} config.
*
* Example usage:
*
* @example
* Ext.create('Ext.form.Panel', {
* title: 'Time Card',
* width: 300,
* bodyPadding: 10,
* renderTo: Ext.getBody(),
* items: [{
* xtype: 'timefield',
* name: 'in',
* fieldLabel: 'Time In',
* minValue: '6:00 AM',
* maxValue: '8:00 PM',
* increment: 30,
* anchor: '100%'
* }, {
* xtype: 'timefield',
* name: 'out',
* fieldLabel: 'Time Out',
* minValue: '6:00 AM',
* maxValue: '8:00 PM',
* increment: 30,
* anchor: '100%'
* }]
* });
*/
Ext.define('Ext.form.field.Time', {
extend: 'Ext.form.field.ComboBox',
alias: 'widget.timefield',
alternateClassName: ['Ext.form.TimeField', 'Ext.form.Time'],
requires: [
'Ext.form.field.Date',
'Ext.picker.Time',
'Ext.view.BoundListKeyNav',
'Ext.Date'
],
/**
* @cfg {String} triggerCls
* An additional CSS class used to style the trigger button. The trigger will always get
* the {@link Ext.form.trigger.Trigger#baseCls} by default and triggerCls will be **appended**
* if specified.
*/
triggerCls: Ext.baseCSSPrefix + 'form-time-trigger',
/**
* @cfg {Date/String} minValue
* The minimum allowed time. Can be either a Javascript date object with a valid time value
* or a string time in a valid format -- see {@link #format} and {@link #altFormats}.
*/
/**
* @cfg {Date/String} maxValue
* The maximum allowed time. Can be either a Javascript date object with a valid time value
* or a string time in a valid format -- see {@link #format} and {@link #altFormats}.
*/
/**
* @cfg {String} minText
* The error text to display when the entered time is before {@link #minValue}.
* @locale
*/
minText: "The time in this field must be equal to or after {0}",
/**
* @cfg {String} maxText
* The error text to display when the entered time is after {@link #maxValue}.
* @locale
*/
maxText: "The time in this field must be equal to or before {0}",
/**
* @cfg {String} invalidText
* The error text to display when the time in the field is invalid.
* @locale
*/
invalidText: "{0} is not a valid time",
/**
* @cfg {String} format
* The default time format string which can be overridden for localization support.
* The format must be valid according to {@link Ext.Date#parse}.
*
* Defaults to `'g:i A'`, e.g., `'3:15 PM'`. For 24-hour time format try `'H:i'` instead.
* @locale
*/
format: "g:i A",
/**
* @cfg {String} [submitFormat=undefined]
* The date format string which will be submitted to the server. The format must be valid
* according to
* {@link Ext.Date#parse}.
*
* Defaults to {@link #format}.
* @locale
*/
/**
* @cfg {String} altFormats
* Multiple date formats separated by "|" to try when parsing a user input value
* and it doesn't match the defined format.
* @locale
*/
// eslint-disable-next-line max-len
altFormats: "g:ia|g:iA|g:i a|g:i A|h:i|g:i|H:i|ga|ha|gA|h a|g a|g A|gi|hi|gia|hia|g|H|gi a|hi a|giA|hiA|gi A|hi A",
/**
* @cfg {String} formatText
* The format text to be announced by screen readers when the field is focused.
* @locale
*/
formatText: 'Expected time format HH:MM space AM or PM',
/**
* @cfg {Number} increment
* The number of minutes between each time value in the list.
*
* Note that this only affects the *list of suggested times.*
*
* To enforce that only times on the list are valid, use {@link #snapToIncrement}.
* That will coerce any typed values to the nearest increment point upon blur.
*/
increment: 15,
/**
* @cfg {Number} pickerMaxHeight
* The maximum height of the {@link Ext.picker.Time} dropdown.
*/
pickerMaxHeight: 300,
/**
* @cfg {Boolean} selectOnTab
* Whether the Tab key should select the currently highlighted item.
*/
selectOnTab: true,
/**
* @cfg {Boolean} snapToIncrement
* Specify as `true` to enforce that only values on the {@link #increment} boundary
* are accepted.
*
* Typed values will be coerced to the nearest {@link #increment} point on blur.
*/
snapToIncrement: false,
/**
* @cfg valuePublishEvent
* @inheritdoc
*/
valuePublishEvent: ['select', 'blur'],
/**
* @private
* This is the date to use when generating time values in the absence of either minValue
* or maxValue. Using the current date causes DST issues on DST boundary dates, so this is an
* arbitrary "safe" date that can be any date aside from DST boundary dates.
*/
initDate: '1/1/2008',
initDateParts: [2008, 0, 1],
initDateFormat: 'j/n/Y',
/**
* @cfg queryMode
* @inheritdoc
*/
queryMode: 'local',
/**
* @cfg displayField
* @inheritdoc
*/
displayField: 'disp',
/**
* @cfg valueField
* @inheritdoc
*/
valueField: 'date',
initComponent: function() {
var me = this,
min = me.minValue,
max = me.maxValue;
if (min) {
me.setMinValue(min);
}
if (max) {
me.setMaxValue(max);
}
/* eslint-disable indent, max-len */
me.displayTpl = new Ext.XTemplate(
'<tpl for=".">' +
'{[typeof values === "string" ? values : this.formatDate(values["' + me.displayField + '"])]}' +
'<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' +
'</tpl>', {
formatDate: me.formatDate.bind(me)
});
/* eslint-enable indent, max-len */
// Create a store of times.
me.store = Ext.picker.Time.createStore(me.format, me.increment);
me.callParent();
// Ensure time constraints are applied to the store.
// TimePicker does this on create.
me.getPicker();
},
afterQuery: function(queryPlan) {
var me = this;
me.callParent([queryPlan]);
// Check the field for null value (TimeField returns null for invalid dates).
// If value is null and a rawValue is present, then we we should manually
// validate the field to display errors.
if (me.value === null && me.getRawValue() && me.validateOnChange) {
me.validate();
}
},
/**
* @private
*/
isEqual: function(v1, v2) {
var fromArray = Ext.Array.from,
isEqual = Ext.Date.isEqual,
i, len;
v1 = fromArray(v1);
v2 = fromArray(v2);
len = v1.length;
if (len !== v2.length) {
return false;
}
for (i = 0; i < len; i++) {
if (!(v2[i] instanceof Date) || !(v1[i] instanceof Date) || !isEqual(v2[i], v1[i])) {
return false;
}
}
return true;
},
/**
* Replaces any existing {@link #minValue} with the new time and refreshes the picker's range.
* @param {Date/String} value The minimum time that can be selected
*/
setMinValue: function(value) {
var me = this,
picker = me.picker;
me.setLimit(value, true);
if (picker) {
picker.setMinValue(me.minValue);
}
},
/**
* Replaces any existing {@link #maxValue} with the new time and refreshes the picker's range.
* @param {Date/String} value The maximum time that can be selected
*/
setMaxValue: function(value) {
var me = this,
picker = me.picker;
me.setLimit(value, false);
if (picker) {
picker.setMaxValue(me.maxValue);
}
},
/**
* @private
* Updates either the min or max value. Converts the user's value into a Date object whose
* year/month/day is set to the {@link #initDate} so that only the time fields are significant.
*/
setLimit: function(value, isMin) {
var me = this,
d, val;
if (Ext.isString(value)) {
d = me.parseDate(value);
}
else if (Ext.isDate(value)) {
d = value;
}
if (d) {
val = me.getInitDate();
val.setHours(d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
}
// Invalid min/maxValue config should result in a null so that defaulting takes over
else {
val = null;
}
me[isMin ? 'minValue' : 'maxValue'] = val;
},
getInitDate: function(hours, minutes, seconds) {
var parts = this.initDateParts;
return new Date(parts[0], parts[1], parts[2], hours || 0, minutes || 0, seconds || 0, 0);
},
valueToRaw: function(value) {
return this.formatDate(this.parseDate(value));
},
/**
* Runs all of Time's validations and returns an array of any errors. Note that this first
* runs Text's validations, so the returned array is an amalgamation of all field errors.
* The additional validation checks are testing that the time format is valid, that the chosen
* time is within the {@link #minValue} and {@link #maxValue} constraints set.
* @param {Object} [value] The value to get errors for (defaults to the current field value)
* @return {String[]} All validation errors for this field
*/
getErrors: function(value) {
value = arguments.length > 0 ? value : this.getRawValue();
// eslint-disable-next-line vars-on-top
var me = this,
format = Ext.String.format,
errors = me.callParent([value]),
minValue = me.minValue,
maxValue = me.maxValue,
data = me.displayTplData,
raw = me.getRawValue(),
i, len, date, item;
if (data && data.length > 0) {
for (i = 0, len = data.length; i < len; i++) {
item = data[i];
item = item.date || item.disp;
date = me.parseDate(item);
if (!date) {
errors.push(format(me.invalidText, item, Ext.Date.unescapeFormat(me.format)));
continue;
}
}
}
else if (raw.length) {
date = me.parseDate(raw);
if (!date) {
// If we don't have any data & a rawValue, it means an invalid time was entered.
errors.push(format(me.invalidText, raw, Ext.Date.unescapeFormat(me.format)));
}
}
// if we have a valid date, we need to check if it's within valid range
// this is out of the loop because as the user types a date/time, the value
// needs to be converted before it can be compared to min/max value
if (!errors.length) {
if (minValue && date < minValue) {
errors.push(format(me.minText, me.formatDate(minValue)));
}
if (maxValue && date > maxValue) {
errors.push(format(me.maxText, me.formatDate(maxValue)));
}
}
return errors;
},
formatDate: function(items) {
var formatted = [],
i, len;
items = Ext.Array.from(items);
for (i = 0, len = items.length; i < len; i++) {
formatted.push(Ext.form.field.Date.prototype.formatDate.call(this, items[i]));
}
return formatted.join(this.delimiter);
},
/**
* @private
* Parses an input value into a valid Date object.
* @param {String/Date} value
*/
parseDate: function(value) {
var me = this,
val = value,
altFormats = me.altFormats,
altFormatsArray = me.altFormatsArray,
i = 0,
len;
if (value && !Ext.isDate(value)) {
val = me.safeParse(value, me.format);
if (!val && altFormats) {
altFormatsArray = altFormatsArray || altFormats.split('|');
len = altFormatsArray.length;
for (; i < len && !val; ++i) {
val = me.safeParse(value, altFormatsArray[i]);
}
}
}
// If configured to snap, snap resulting parsed Date to the closest increment.
if (val && me.snapToIncrement) {
val = new Date(Ext.Number.snap(val.getTime(), me.increment * 60 * 1000));
}
return val;
},
safeParse: function(value, format) {
var me = this,
utilDate = Ext.Date,
parsedDate,
result = null;
if (utilDate.formatContainsDateInfo(format)) {
// assume we've been given a full date
result = utilDate.parse(value, format);
}
else {
// Use our initial safe date
parsedDate =
utilDate.parse(me.initDate + ' ' + value, me.initDateFormat + ' ' + format);
if (parsedDate) {
result = parsedDate;
}
}
return result;
},
/**
* @private
*/
getSubmitValue: function() {
var me = this,
format = me.submitFormat || me.format,
value = me.getValue();
return value ? Ext.Date.format(value, format) : null;
},
/**
* @private
* Creates the {@link Ext.picker.Time}
*/
createPicker: function() {
var me = this;
me.listConfig = Ext.apply({
xtype: 'timepicker',
pickerField: me,
cls: undefined,
minValue: me.minValue,
maxValue: me.maxValue,
increment: me.increment,
format: me.format,
maxHeight: me.pickerMaxHeight
}, me.listConfig);
return me.callParent();
},
completeEdit: function() {
var me = this,
val = me.getValue();
me.callParent(arguments);
// Only set the raw value if the current value is valid and is not falsy
if (me.validateValue(val)) {
me.setValue(val);
}
},
/**
* Finds the record by searching values in the {@link #valueField}.
* @param {Object/String} value The value to match the field against.
* @return {Ext.data.Model} The matched record or false.
*/
findRecordByValue: function(value) {
if (typeof value === 'string') {
value = this.parseDate(value);
}
return this.callParent([value]);
},
rawToValue: function(item) {
var me = this,
items, values, i, len;
if (me.multiSelect) {
values = [];
items = Ext.Array.from(item);
for (i = 0, len = items.length; i < len; i++) {
values.push(me.parseDate(items[i]));
}
return values;
}
return me.parseDate(item);
},
setValue: function(v) {
var me = this;
// The timefield can get in a loop when creating its picker. For instance, when creating
// the picker, the timepicker will add a filter (see TimePicker#updateList) which will
// then trigger the checkValueOnChange listener which in turn calls into here,
// rinse and repeat.
if (me.creatingPicker) {
return;
}
// Store MUST be created for parent setValue to function.
me.getPicker();
if (Ext.isDate(v)) {
v = me.getInitDate(v.getHours(), v.getMinutes(), v.getSeconds());
}
return me.callParent([v]);
},
getValue: function() {
return this.rawToValue(this.callParent(arguments));
}
});