/**
* Represents a grouping of items. The grouper works in a similar fashion as the
* `Ext.util.Sorter` except that groups must be able to extract a value by which all items
* in the group can be collected. By default this is derived from the `property` config
* but can be customized using the `groupFn` if necessary.
*
* All items with the same group value compare as equal. If the group values do not compare
* equally, the sort can be controlled further by setting `sortProperty` or `sorterFn`.
*/
Ext.define('Ext.util.Grouper', {
extend: 'Ext.util.Sorter',
isGrouper: true,
config: {
/**
* @cfg {Function} groupFn This function is called for each item in the collection
* to determine the group to which it belongs. By default the `property` value is
* used to group items.
* @cfg {Object} groupFn.item The current item from the collection.
* @cfg {String} groupFn.return The group identifier for the item.
*/
groupFn: null,
/**
* @cfg {String} property The field by which records are grouped. Groups are
* sorted alphabetically by group value as the default. To sort groups by a different
* property, use the {@link #sortProperty} configuration.
*/
/**
* @cfg {String} sortProperty You can set this configuration if you want the groups
* to be sorted on something other then the group string returned by the `groupFn`.
* This serves the same role as `property` on a normal `Ext.util.Sorter`.
*/
sortProperty: null,
/**
* @cfg {String} formatter
* This config accepts a format specification as would be used in a `Ext.Template`
* formatted token. For example `'round(2)'` to round numbers to 2 decimal places
* or `'date("Y-m-d")'` to format a Date.
*
* It is used to format the group name. Can be used instead of the `groupFn` config.
*/
formatter: false,
/**
* @cfg {String} blankValue
*
* A text that is used if the generated name for the group is empty
*/
blankValue: ''
},
_eventToMethodMap: {
propertychange: 'onGrouperPropertyChange',
directionchange: 'onGrouperDirectionChange'
},
constructor: function(config) {
//<debug>
if (config) {
if (config.getGroupString) {
Ext.raise("Cannot set getGroupString - use groupFn instead");
}
}
//</debug>
this.callParent(arguments);
},
/**
* Returns the string value for grouping, primarily used for grouper key.
* @param {Ext.data.Model} item The Model instance
* @return {String}
*/
getGroupString: function(item) {
return item.$collapsedGroupPlaceholder
? item.$groupKey
: this.getGroupValue(item).toString();
},
/**
* Returns the value for grouping to be used.
* @param {Ext.data.Model} item The Model instance
* @return {Mixed}
*/
getGroupValue: function(item) {
var groupValue = item.$collapsedGroupPlaceholder ? item.$groupValue : this._groupFn(item);
return (groupValue != null && groupValue !== '') ? groupValue : this.getBlankValue();
},
sortFn: function(item1, item2) {
var me = this,
lhs = me.getGroupValue(item1),
rhs = me.getGroupValue(item2),
property = me._sortProperty, // Sorter's sortFn uses "_property"
root = me._root,
sorterFn = me._sorterFn,
transform = me._transform;
// Compare groupFn results for both sides and return if they are equal, ensuring
// correct comparison in case values are dates.
if (lhs === rhs || Ext.Date.isEqual(lhs, rhs)) {
return 0;
}
if (property || sorterFn) {
if (sorterFn) {
return sorterFn.call(this, item1, item2);
}
if (root) {
item1 = item1[root];
item2 = item2[root];
}
lhs = item1[property];
rhs = item2[property];
if (transform) {
lhs = transform(lhs);
rhs = transform(rhs);
}
}
return (lhs > rhs) ? 1 : (lhs < rhs ? -1 : 0);
},
standardGroupFn: function(item) {
var me = this,
root = me._root,
formatter = me._formatter,
value = (root ? item[root] : item)[me._property];
if (formatter) {
value = formatter(value, me);
}
return value;
},
updateSorterFn: function() {
// don't callParent here - we don't want to smash sortFn w/sorterFn
},
updateProperty: function(data, oldData) {
var me = this;
// we don't callParent since that is related to sorterFn smashing sortFn
if (!me.getGroupFn()) {
me.setGroupFn(me.standardGroupFn);
}
me.notify('propertychange', [data, oldData]);
},
updateDirection: function(data, oldData) {
this.callParent([data, oldData]);
this.notify('directionchange', [data, oldData]);
},
applyFormatter: function(value) {
var parser, format;
if (!value) {
return null;
}
parser = Ext.app.bind.Parser.fly(value);
format = parser.compileFormat();
parser.release();
return function(v, scope) {
return format(v, scope);
};
},
addObserver: function(observer) {
var me = this,
observers = me.observers;
if (!observers) {
me.observers = observers = [];
}
if (!Ext.Array.contains(observers, observer)) {
// if we're in the middle of notifying, we need to clone the observers
if (me.notifying) {
me.observers = observers = observers.slice(0);
}
observers[observers.length] = observer;
}
me.dirtyObservers = true;
},
prioritySortFn: function(o1, o2) {
var a = +o1.observerPriority,
b = +o2.observerPriority;
if (isNaN(a)) {
a = 0;
}
if (isNaN(b)) {
b = 0;
}
return a - b;
},
removeObserver: function(observer) {
var observers = this.observers;
if (observers) {
Ext.Array.remove(observers, observer);
this.dirtyObservers = true;
}
},
clearObservers: function() {
this.observers = null;
},
notify: function(eventName, args) {
var me = this,
observers = me.observers,
methodName = me._eventToMethodMap[eventName],
added = 0,
index, method, observer;
args = args || [];
if (observers && methodName) {
me.notifying = true;
if (me.dirtyObservers && observers.length > 1) {
// Allow observers to be inserted with a priority.
// For example GroupCollections must react to Collection mutation before views.
// Before notifying our observers let's sort them by priority.
Ext.Array.sort(observers, me.prioritySortFn);
me.dirtyObservers = false;
}
for (index = 0; index < observers.length; ++index) {
method = (observer = observers[index])[methodName];
if (method) {
if (!added++) { // jshint ignore:line
args.unshift(me);
}
method.apply(observer, args);
}
}
me.notifying = false;
}
}
});