/**
* A mixin which allows a data component to be sorted. This is used by e.g. {@link Ext.data.Store}
* and {@link Ext.data.TreeStore}.
*
* **NOTE**: This mixin is mainly for internal use and most users should not need to use it
* directly. It is more likely you will want to use one of the component classes that import
* this mixin, such as {@link Ext.data.Store} or {@link Ext.data.TreeStore}.
*/
Ext.define("Ext.util.Sortable", {
/**
* @property {Boolean} isSortable
* `true` in this class to identify an object as an instantiated Sortable, or subclass thereof.
*/
isSortable: true,
$configPrefixed: false,
$configStrict: false,
config: {
/**
* @cfg {Ext.util.Sorter[]/Object[]} sorters
* The initial set of {@link Ext.util.Sorter Sorters}.
*
* sorters: [{
* property: 'age',
* direction: 'DESC'
* }, {
* property: 'firstName',
* direction: 'ASC'
* }]
*/
sorters: null
},
/**
* @cfg {String} defaultSortDirection
* The default sort direction to use if one is not specified.
*/
defaultSortDirection: "ASC",
requires: [
'Ext.util.Sorter'
],
/**
* @event beforesort
* Fires before a sort occurs.
* @param {Ext.util.Sortable} me This object.
* @param {Ext.util.Sorter[]} sorters The collection of Sorters being used to generate
* the comparator function.
*/
/**
* @cfg {Number} [multiSortLimit=3]
* The maximum number of sorters which may be applied to this Sortable when using the "multi"
* insertion position when adding sorters.
*
* New sorters added using the "multi" insertion position are inserted at the top of the
* sorters list becoming the new primary sort key.
*
* If the sorters collection has grown to longer then **`multiSortLimit`**, then it is trimmed.
*
*/
multiSortLimit: 3,
statics: {
/**
* Creates a single comparator function which encapsulates the passed Sorter array.
* @param {Ext.util.Sorter[]} sorters The sorter set for which to create a comparator
* function
* @return {Function} a function, which when passed two comparable objects returns
* the result of the whole sorter comparator functions.
*/
createComparator: function(sorters) {
return sorters && sorters.length
? function(r1, r2) {
var result = sorters[0].sort(r1, r2),
length = sorters.length,
i = 1;
// While we have not established a comparison value,
// loop through subsequent sorters asking for a comparison value
for (; !result && i < length; i++) {
result = sorters[i].sort.call(sorters[i], r1, r2);
}
return result;
}
: function() {
return 0;
};
}
},
/**
* @cfg {String} sortRoot
* The property in each item that contains the data to sort.
*/
applySorters: function(sorters) {
var me = this,
sortersCollection;
sortersCollection = me.getSorters() || new Ext.util.MixedCollection(false, Ext.returnId);
// We have been configured with a non-default value.
if (sorters) {
sortersCollection.addAll(me.decodeSorters(sorters));
}
return sortersCollection;
},
/**
* Updates the sorters collection and triggers sorting of this Sortable. Example usage:
*
* //sort by a single field
* myStore.sort('myField', 'DESC');
*
* //sorting by multiple fields
* myStore.sort([{
* property : 'age',
* direction: 'ASC'
* }, {
* property : 'name',
* direction: 'DESC'
* }]);
*
* Classes which use this mixin must implement a **`soSort`** method which accepts a comparator
* function computed from the full sorter set which performs the sort
* in an implementation-specific way.
*
* When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field,
* so this code:
*
* store.sort('myField');
* store.sort('myField');
*
* Is equivalent to this code, because Store handles the toggling automatically:
*
* store.sort('myField', 'ASC');
* store.sort('myField', 'DESC');
*
* @param {String/Ext.util.Sorter[]} [sorters] Either a string name of one of the fields
* in this Store's configured {@link Ext.data.Model Model}, or an array of sorter
* configurations.
* @param {String} [direction="ASC"] The overall direction to sort the data by.
* @param {String} [insertionPosition="replace"] Where to put the new sorter in the collection
* of sorters. This may take the following values:
*
* * `replace`: This means that the new sorter(s) becomes the sole sorter set for this Sortable.
* This is the most useful call mode to programatically sort by multiple fields.
*
* * `prepend`: This means that the new sorters are inserted as the primary sorters, unchanged,
* and the sorter list length must be controlled by the developer.
*
* * `multi`: This is mainly useful for implementing intuitive "Sort by this" user interfaces
* such as the {@link Ext.grid.Panel GridPanel}'s column sorting UI. This mode is only
* supported when passing a property name and a direction. This means that the new sorter
* becomes the primary sorter. If the sorter was **already** the primary sorter, the direction
* of sort is toggled if no direction parameter is specified. The number of sorters maintained
* is limited by the {@link #multiSortLimit} configuration.
*
* * `append` : This means that the new sorter becomes the last sorter.
* @param {Boolean} doSort True to sort using a generated sorter function that combines all
* of the Sorters passed
* @return {Ext.util.Sorter[]} The new sorters.
*/
sort: function(sorters, direction, insertionPosition, doSort) {
var me = this,
sorter,
overFlow,
currentSorters = me.getSorters();
if (!currentSorters) {
me.setSorters(null);
currentSorters = me.getSorters();
}
if (Ext.isArray(sorters)) {
doSort = insertionPosition;
insertionPosition = direction;
}
else if (Ext.isObject(sorters)) {
sorters = [sorters];
doSort = insertionPosition;
insertionPosition = direction;
}
else if (Ext.isString(sorters)) {
sorter = currentSorters.get(sorters);
if (!sorter) {
sorter = {
property: sorters,
direction: direction
};
}
else if (direction == null) {
sorter.toggle();
}
else {
sorter.setDirection(direction);
}
sorters = [sorter];
}
if (sorters && sorters.length) {
sorters = me.decodeSorters(sorters);
switch (insertionPosition) {
// multi sorting means always inserting the specified sorters
// at the top.
// If we are asked to sort by what is already the primary sorter
// then toggle its direction.
case "multi":
// Insert the new sorter at the beginning.
currentSorters.insert(0, sorters[0]);
// If we now are oversize, trim our sorters collection
overFlow = currentSorters.getCount() - me.multiSortLimit;
if (overFlow > 0) {
currentSorters.removeRange(me.multiSortLimit, overFlow);
}
break;
case "prepend":
currentSorters.insert(0, sorters);
break;
case "append":
currentSorters.addAll(sorters);
break;
case undefined:
case null:
case "replace":
currentSorters.clear();
currentSorters.addAll(sorters);
break;
default:
//<debug>
Ext.raise('Sorter insertion point must be "multi", "prepend", ' +
'"append" or "replace"');
//</debug>
}
}
if (doSort !== false) {
me.fireEvent('beforesort', me, sorters);
me.onBeforeSort(sorters);
if (me.getSorterCount()) {
// Sort using a generated sorter function which combines all of the Sorters passed
me.doSort(me.generateComparator());
}
}
return sorters;
},
/**
* @protected
* Returns the number of Sorters which apply to this Sortable.
*
* May be overridden in subclasses. {@link Ext.data.Store Store} in particlar overrides
* this because its groupers must contribute to the sorter count so that the sort method above
* executes doSort.
*/
getSorterCount: function() {
return this.getSorters().items.length;
},
/**
* Returns a comparator function which compares two items and returns -1, 0, or 1 depending
* on the currently defined set of {@link #cfg-sorters}.
*
* If there are no {@link #cfg-sorters} defined, it returns a function which returns `0` meaning
* that no sorting will occur.
*/
generateComparator: function() {
var sorters = this.getSorters().getRange();
return sorters.length ? this.createComparator(sorters) : this.emptyComparator;
},
emptyComparator: function() {
return 0;
},
onBeforeSort: Ext.emptyFn,
/**
* @private
* Normalizes an array of sorter objects, ensuring that they are all Ext.util.Sorter instances
* @param {Object[]} sorters The sorters array
* @return {Ext.util.Sorter[]} Array of Ext.util.Sorter objects
*/
decodeSorters: function(sorters) {
if (!Ext.isArray(sorters)) {
if (sorters === undefined) {
sorters = [];
}
else {
sorters = [sorters];
}
}
// eslint-disable-next-line vars-on-top
var length = sorters.length,
Sorter = Ext.util.Sorter,
model = this.getModel ? this.getModel() : this.model,
field,
config, i;
for (i = 0; i < length; i++) {
config = sorters[i];
if (!(config instanceof Sorter)) {
if (Ext.isString(config)) {
config = {
property: config
};
}
Ext.applyIf(config, {
root: this.sortRoot,
direction: "ASC"
});
// support for 3.x style sorters where a function can be defined as 'fn'
if (config.fn) {
config.sorterFn = config.fn;
}
// support a function to be passed as a sorter definition
if (typeof config === 'function') {
config = {
sorterFn: config
};
}
// ensure sortType gets pushed on if necessary
if (model && !config.transform) {
field = model.getField(config.property);
config.transform = field && field.sortType !== Ext.identityFn
? field.sortType
: undefined;
}
sorters[i] = new Ext.util.Sorter(config);
}
}
return sorters;
},
/**
* Gets the first sorter from the sorters collection, excluding
* any groupers that may be in place
* @protected
* @return {Ext.util.Sorter} The sorter, null if none exist
*/
getFirstSorter: function() {
var sorters = this.getSorters().items,
len = sorters.length,
i = 0,
sorter;
for (; i < len; ++i) {
sorter = sorters[i];
if (!sorter.isGrouper) {
return sorter;
}
}
return null;
}
}, function() {
// Reference the static implementation in prototype
this.prototype.createComparator = this.createComparator;
});