/**
* Filter by a configurable Ext.picker.DatePicker menu
*
* This filter allows for the following configurations:
*
* - Any of the normal configs will be passed through to either component.
* - There can be a docked config.
* - The timepicker can be on the right or left (datepicker, too, of course).
* - Choose which component will initiate the filtering, i.e., the event can be
* configured to be bound to either the datepicker or the timepicker, or if
* there is a docked config it be automatically have the handler bound to it.
*
* Although not shown here, this class accepts all configuration options
* for {@link Ext.picker.Date} and {@link Ext.picker.Time}.
*
* In the case that a custom dockedItems config is passed in, the
* class will handle binding the default listener to it so the
* developer need not worry about having to do it.
*
* The default dockedItems position and the toolbar's
* button text can be passed a config for convenience, i.e.,:
*
* dock: {
* buttonText: 'Click to Filter',
* dock: 'left'
* }
*
* Or, pass in a full dockedItems config:
*
* dock: {
* dockedItems: {
* xtype: 'toolbar',
* dock: 'bottom',
* ...
* }
* }
*
* Or, give a value of `true` to accept dock defaults:
*
* dock: true
*
* But, it must be one or the other.
*
* Example Usage:
*
* var filters = Ext.create('Ext.ux.grid.GridFilters', {
* ...
* filters: [{
* // required configs
* type: 'datetime',
* dataIndex: 'date',
*
* // optional configs
* positionDatepickerFirst: false,
* //selectDateToFilter: false, // this is overridden b/c of the presence of the dock cfg object
*
* date: {
* format: 'm/d/Y',
* },
*
* time: {
* format: 'H:i:s A',
* increment: 1
* },
*
* dock: {
* buttonText: 'Click to Filter',
* dock: 'left'
*
* // allows for custom dockedItems cfg
* //dockedItems: {}
* }
* }]
* });
*
* In the above example, note that the filter is being passed a {@link #date} config object,
* a {@link #time} config object and a {@link #dock} config. These are all optional.
*
* As for positioning, the datepicker will be on the right, the timepicker on the left
* and the docked items will be docked on the left. In addition, since there's a {@link #dock}
* config, clicking the button in the dock will trigger the filtering.
*/
Ext.define('Ext.ux.grid.filter.DateTimeFilter', {
extend: 'Ext.ux.grid.filter.DateFilter',
alias: 'gridfilter.datetime',
/**
* @private
*/
dateDefaults: {
xtype: 'datepicker',
format: 'm/d/Y'
},
/**
* @private
*/
timeDefaults: {
xtype: 'timepicker',
width: 100,
height: 200,
format: 'g:i A'
},
/**
* @private
*/
dockDefaults: {
dock: 'top',
buttonText: 'Filter'
},
/**
* @cfg {Object} date
* A {@link Ext.picker.Date} can be configured here.
* Uses {@link #dateDefaults} by default.
*/
/**
* @cfg {Object} time
* A {@link Ext.picker.Time} can be configured here.
* Uses {@link #timeDefaults} by default.
*/
/**
* @cfg {Boolean/Object} dock
* A {@link Ext.panel.AbstractPanel#cfg-dockedItems} can be configured here.
* A `true` value will use the {@link #dockDefaults} default configuration.
* If present, the button in the docked items will initiate the filtering.
*/
/**
* @cfg {Boolean} [selectDateToFilter=true]
* By default, the datepicker has the default event listener bound to it.
* Setting to `false` will bind it to the timepicker.
*
* The config will be ignored if there is a `dock` config.
*/
selectDateToFilter: true,
/**
* @cfg {Boolean} [positionDatepickerFirst=true]
* Positions the datepicker within its container.
* A `true` value will place it on the left in the container.
* Set to `false` if the timepicker should be placed on the left.
* Defaults to `true`.
*/
positionDatepickerFirst: true,
reTime: /\s(am|pm)/i,
reItemId: /\w*-(\w*)$/,
/**
* Replaces the selected value of the timepicker with the default 00:00:00.
* @private
* @param {Object} date
* @param {Ext.picker.Time} timepicker
* @return Date object
*/
addTimeSelection: function (date, timepicker) {
var me = this,
selection = timepicker.getSelectionModel().getSelection(),
time, len, fn, val,
i = 0,
arr = [],
timeFns = ['setHours', 'setMinutes', 'setSeconds', 'setMilliseconds'];
if (selection.length) {
time = selection[0].get('disp');
// Loop through all of the splits and add the time values.
arr = time.replace(me.reTime, '').split(':');
for (len = arr.length; i < len; i++) {
fn = timeFns[i];
val = arr[i];
if (val) {
date[fn](parseInt(val, 10));
}
}
}
return date;
},
/**
* @private
* Template method that is to initialize the filter and install required menu items.
*/
init: function (config) {
var me = this,
dateCfg = Ext.applyIf(me.date || {}, me.dateDefaults),
timeCfg = Ext.applyIf(me.time || {}, me.timeDefaults),
dockCfg = me.dock, // should not default to empty object
defaultListeners = {
click: {
scope: me,
click: me.onMenuSelect
},
select: {
scope: me,
select: me.onMenuSelect
}
},
pickerCtnCfg, i, len, item, cfg,
items = [dateCfg, timeCfg],
// we need to know the datepicker's position in the items array
// for when the itemId name is bound to it before adding to the menu
datepickerPosition = 0;
if (!me.positionDatepickerFirst) {
items = items.reverse();
datepickerPosition = 1;
}
pickerCtnCfg = Ext.apply(me.pickerOpts, {
xtype: !dockCfg ? 'container' : 'panel',
layout: 'hbox',
items: items
});
// If there's no dock config then bind the default listener to the desired picker.
if (!dockCfg) {
if (me.selectDateToFilter) {
dateCfg.listeners = defaultListeners.select;
} else {
timeCfg.listeners = defaultListeners.select;
}
} else if (dockCfg) {
me.selectDateToFilter = null;
if (dockCfg.dockedItems) {
pickerCtnCfg.dockedItems = dockCfg.dockedItems;
// TODO: allow config that will tell which item to bind the listener to
// right now, it's using the first item
pickerCtnCfg.dockedItems.items[dockCfg.bindToItem || 0].listeners = defaultListeners.click;
} else {
// dockCfg can be `true` if button text and dock position defaults are wanted
if (Ext.isBoolean(dockCfg)) {
dockCfg = {};
}
dockCfg = Ext.applyIf(dockCfg, me.dockDefaults);
pickerCtnCfg.dockedItems = {
xtype: 'toolbar',
dock: dockCfg.dock,
items: [
{
xtype: 'button',
text: dockCfg.buttonText,
flex: 1,
listeners: defaultListeners.click
}
]
};
}
}
me.fields = {};
for (i = 0, len = me.menuItems.length; i < len; i++) {
item = me.menuItems[i];
if (item !== '-') {
pickerCtnCfg.items[datepickerPosition].itemId = item;
cfg = {
itemId: 'range-' + item,
text: me[item + 'Text'],
menu: Ext.create('Ext.menu.Menu', {
items: pickerCtnCfg
}),
listeners: {
scope: me,
checkchange: me.onCheckChange
}
};
item = me.fields[item] = Ext.create('Ext.menu.CheckItem', cfg);
}
me.menu.add(item);
}
me.values = {};
},
/**
* @private
*/
onCheckChange: function (item, checked) {
var me = this,
menu = item.menu,
timepicker = menu.down('timepicker'),
datepicker = menu.down('datepicker'),
itemId = datepicker.itemId,
values = me.values;
if (checked) {
values[itemId] = me.addTimeSelection(datepicker.value, timepicker);
} else {
delete values[itemId];
}
me.setActive(me.isActivatable());
me.fireEvent('update', me);
},
* Handler for when the DatePicker for a field fires the 'select' event
* @param {Ext.picker.Date} picker
* @param {Object} date
*/
onMenuSelect: function (picker, date) {
// NOTE: we need to redefine the picker.
var me = this,
menu = me.menu,
checkItemId = menu.getFocusEl().itemId.replace(me.reItemId, '$1'),
fields = me.fields,
field;
picker = menu.queryById(checkItemId);
field = me.fields[picker.itemId];
field.setChecked(true);
if (field == fields.on) {
fields.before.setChecked(false, true);
fields.after.setChecked(false, true);
} else {
fields.on.setChecked(false, true);
if (field == fields.after && me.getFieldValue('before') < date) {
fields.before.setChecked(false, true);
} else if (field == fields.before && me.getFieldValue('after') > date) {
fields.after.setChecked(false, true);
}
}
me.fireEvent('update', me);
// The timepicker's getBubbleTarget() returns the boundlist's implementation,
// so it doesn't look up ownerCt chain (it looks up this.pickerField).
// This is a problem :)
// This can be fixed by just walking up the ownerCt chain
// (same thing, but confusing without comment).
picker.ownerCt.ownerCt.hide();
},
/**
* @private
* Template method that is to get and return serialized filter data for
* transmission to the server.
* @return {Object/Array} An object or collection of objects containing
* key value pairs representing the current configuration of the filter.
*/
getSerialArgs: function () {
var me = this,
key,
fields = me.fields,
args = [];
for (key in fields) {
if (fields[key].checked) {
args.push({
type: 'datetime',
comparison: me.compareMap[key],
value: Ext.Date.format(me.getFieldValue(key), (me.date.format || me.dateDefaults.format) + ' ' + (me.time.format || me.timeDefaults.format))
});
}
}
return args;
},
/**
* @private
* Template method that is to set the value of the filter.
* @param {Object} value The value to set the filter
* @param {Boolean} preserve true to preserve the checked status
* of the other fields. Defaults to false, unchecking the
* other fields
*/
setValue: function (value, preserve) {
var me = this,
fields = me.fields,
key,
val,
datepicker;
for (key in fields) {
val = value[key];
if (val) {
datepicker = me.menu.down('datepicker[itemId="' + key + '"]');
// Note that calling the Ext.picker.Date:setValue() calls Ext.Date.clearTime(),
// which we don't want, so just call update() instead and set the value on the component.
datepicker.update(val);
datepicker.value = val;
fields[key].setChecked(true);
} else if (!preserve) {
fields[key].setChecked(false);
}
}
me.fireEvent('update', me);
},
/**
* Template method that is to validate the provided Ext.data.Record
* against the filters configuration.
* @param {Ext.data.Record} record The record to validate
* @return {Boolean} true if the record is valid within the bounds
* of the filter, false otherwise.
*/
validateRecord: function (record) {
// remove calls to Ext.Date.clearTime
var me = this,
key,
pickerValue,
val = record.get(me.dataIndex);
if(!Ext.isDate(val)){
return false;
}
val = val.getTime();
for (key in me.fields) {
if (me.fields[key].checked) {
pickerValue = me.getFieldValue(key).getTime();
if (key == 'before' && pickerValue <= val) {
return false;
}
if (key == 'after' && pickerValue >= val) {
return false;
}
if (key == 'on' && pickerValue != val) {
return false;
}
}
}
return true;
}
});