/**
* This class provides a flexible means to control the
* `{@link Ext.util.Collection#cfg!sorters sorters}` of a
* `{@link Ext.util.Collection Collection}`. Instances of this class are created
* automatically when sorters are added to added to Collections.
*
* This collection can be directly manipulated by application code to gain full
* control over the sorters of the owner collection. Be aware that some components
* create sorters internally (such as grids) so be careful in such cases to not disturb
* these additional sorters.
*
* Items in this collection are `Ext.util.Sorter` instances and can be managed
* individually by their `id`. This is the recommended way to manage application
* filters while preserving sorter applied from other sources.
*
* Bulk changes to this collection should be wrapped in
* `{@link Ext.util.Collection#method!beginUpdate beginUpdate}` and
* `{@link Ext.util.Collection#method!endUpdate endUpdate}` (as with any collection).
* During these bulk updates all reactions to sorter changes will be suspended.
*/
Ext.define('Ext.util.SorterCollection', {
extend: 'Ext.util.Collection',
requires: [
'Ext.util.Sorter'
],
isSorterCollection: true,
/**
* @property {Ext.util.Sortable} sortable
* The owning sortable instance. The sortable's configuration governs this
* collection.
* @private
* @readonly
*/
$sortable: null,
/**
* @property sortFn
* This is the cached sorting function which is a generated function that calls all the
* configured sorters in the correct order.
* @readonly
*/
sortFn: null,
config: {
/**
* @cfg {Function} applySorterOptionsFn
* A template method that can be used to apply options to a sorter during creation
* @private
*/
sorterOptionsFn: null,
/**
* @cfg {Object} applySorterOptionsScope
* The scope to execute the {@link #applySorterOptionsFn}
* @private
*/
sorterOptionsScope: null
},
constructor: function(config) {
var me = this;
me.sortFn = Ext.util.Sorter.createComparator(me);
me.callParent([config]);
me.setDecoder(me.decodeSorter);
},
addSort: function(property, direction, mode) {
var me = this,
count, index, limit, options, primary, sorter, sorters;
if (!property) {
// nothing specified so just trigger a sort...
me.beginUpdate();
me.endUpdate();
}
else {
options = me.getOptions();
if (property instanceof Array) {
sorters = property;
mode = direction;
direction = null;
}
else if (Ext.isString(property)) {
if (!(sorter = me.get(property))) {
sorters = [{
property: property,
direction: direction || options.getDefaultSortDirection()
}];
}
else {
sorters = [sorter];
}
}
else if (Ext.isFunction(property)) {
sorters = [{
sorterFn: property,
direction: direction || options.getDefaultSortDirection()
}];
}
else {
//<debug>
if (!Ext.isObject(property)) {
Ext.raise('Invalid sort descriptor: ' + property);
}
//</debug>
sorters = [property];
mode = direction;
direction = null;
}
//<debug>
if (mode && !me._sortModes[mode]) {
Ext.raise('Sort mode should be "multi", "append", "prepend" or "replace", not "' +
mode + '"');
}
//</debug>
mode = me._sortModes[mode || 'replace'];
primary = me.getAt(0);
count = me.length;
index = mode.append ? count : 0;
// We have multiple changes to make, so mark the sorters collection as updating
// before we start.
me.beginUpdate();
// Leverage the decode logic wired to the collection to up-convert sorters to
// real instances.
me.splice(index, mode.replace ? count : 0, sorters);
if (mode.multi) {
count = me.length;
limit = options.getMultiSortLimit();
if (count > limit) {
me.removeAt(limit, count); // count will be truncated
}
}
if (sorter && direction) {
sorter.setDirection(direction);
}
else if (index === 0 && primary && primary === me.getAt(0)) {
// If we just adjusted the sorters at the front and the primary sorter is
// still the primary sorter, toggle its direction:
primary.toggle();
}
me.endUpdate();
}
},
clear: function() {
// The owning Collection needs to have its onSortersEndUpdate called on sorter clear so that
// it clears its sorted flag.
this.beginUpdate();
this.callParent();
this.endUpdate(this.items);
},
/**
* Returns an up to date sort function.
* @return {Function} The sort function.
*/
getSortFn: function() {
return this.sortFn;
},
/**
* Get the first matching sorter with a matching property.
* @param {String} prop The property name
* @return {Ext.util.Sorter} The sorter. `null` if not found.
* @private
*/
getByProperty: function(prop) {
var items = this.items,
len = items.length,
i, item;
for (i = 0; i < len; ++i) {
item = items[i];
if (item.getProperty() === prop) {
return item;
}
}
return null;
},
//-------------------------------------------------------------------------
// Private
_sortModes: {
append: { append: 1 },
multi: { multi: 1 },
prepend: { prepend: 1 },
replace: { replace: 1 }
},
decodeSorter: function(sorter, xclass) {
var me = this,
options = me.getOptions(),
root = options.getRootProperty(),
sorterOptionsFn = me.getSorterOptionsFn(),
currentSorter, sorterConfig, type;
if (sorter.isSorter) {
if (!sorter.getRoot()) {
sorter.setRoot(root);
}
}
else {
sorterConfig = {
direction: options.getDefaultSortDirection(),
root: root
};
type = typeof sorter;
// If we are dealing with a string we assume it is a property they want to sort on.
if (type === 'string') {
currentSorter = me.get(sorter);
if (currentSorter) {
return currentSorter;
}
sorterConfig.property = sorter;
}
// If it is a function, we assume its a sorting function.
else if (type === 'function') {
sorterConfig.sorterFn = sorter;
}
// If we are dealing with an object, we assume its a Sorter configuration. In
// this case we create an instance of Sorter passing this configuration.
else {
// Finally we get to the point where it has to be invalid
//<debug>
if (!Ext.isObject(sorter)) {
Ext.raise('Invalid sorter specified: ' + sorter);
}
//</debug>
sorterConfig = Ext.apply(sorterConfig, sorter);
if (sorterConfig.fn) {
sorterConfig.sorterFn = sorterConfig.fn;
delete sorterConfig.fn;
}
}
// If a sorter config was created, make it an instance
sorter = Ext.create(xclass || Ext.util.Sorter, sorterConfig);
}
if (sorterOptionsFn) {
sorterOptionsFn.call(me.getSorterOptionsScope() || me, sorter);
}
return sorter;
},
setSorterConfigure: function(fn, scope) {
this.setSorterOptionsFn(fn);
this.setSorterOptionsScope(scope);
},
decodeRemoveItems: function(args, index) {
var me = this,
ret = (index === undefined) ? args : args[index];
if (!ret || !ret.$cloned) {
if (args.length > index + 1 || !Ext.isIterable(ret)) {
ret = Ext.Array.slice(args, index);
}
// eslint-disable-next-line vars-on-top
var currentSorters = me.items,
ln = ret.length,
remove = [],
i, item, n, sorter, type;
for (i = 0; i < ln; i++) {
sorter = ret[i];
if (sorter && sorter.isSorter) {
remove.push(sorter);
}
else {
type = typeof sorter;
if (type === 'string') {
sorter = me.get(sorter);
if (sorter) {
remove.push(sorter);
}
}
else if (type === 'function') {
for (n = currentSorters.length; n-- > 0;) {
item = currentSorters[n];
if (item.getSorterFn() === sorter) {
remove.push(item);
}
}
}
//<debug>
else {
Ext.raise('Invalid sorter specification: ' + sorter);
}
//</debug>
}
}
ret = remove;
ret.$cloned = true;
}
return ret;
},
getOptions: function() {
// Odd thing this. We need a Sortable to know how to manage our collection, but
// we may not have one. Of course as a Collection, we *are* one as well... just
// that is not really useful to sort the sorters themselves, but we do have the
// default options for Sortables baked in, so we'll do.
return this.$sortable || this;
}
});