]> git.proxmox.com Git - extjs.git/blob - extjs/packages/core/src/util/Sortable.js
import ExtJS 7.0.0 GPL
[extjs.git] / extjs / packages / core / src / util / Sortable.js
1 /**
2 * A mixin which allows a data component to be sorted. This is used by e.g. {@link Ext.data.Store}
3 * and {@link Ext.data.TreeStore}.
4 *
5 * **NOTE**: This mixin is mainly for internal use and most users should not need to use it
6 * directly. It is more likely you will want to use one of the component classes that import
7 * this mixin, such as {@link Ext.data.Store} or {@link Ext.data.TreeStore}.
8 */
9 Ext.define("Ext.util.Sortable", {
10 /**
11 * @property {Boolean} isSortable
12 * `true` in this class to identify an object as an instantiated Sortable, or subclass thereof.
13 */
14 isSortable: true,
15
16 $configPrefixed: false,
17 $configStrict: false,
18
19 config: {
20 /**
21 * @cfg {Ext.util.Sorter[]/Object[]} sorters
22 * The initial set of {@link Ext.util.Sorter Sorters}.
23 *
24 * sorters: [{
25 * property: 'age',
26 * direction: 'DESC'
27 * }, {
28 * property: 'firstName',
29 * direction: 'ASC'
30 * }]
31 */
32 sorters: null
33 },
34
35 /**
36 * @cfg {String} defaultSortDirection
37 * The default sort direction to use if one is not specified.
38 */
39 defaultSortDirection: "ASC",
40
41 requires: [
42 'Ext.util.Sorter'
43 ],
44
45 /**
46 * @event beforesort
47 * Fires before a sort occurs.
48 * @param {Ext.util.Sortable} me This object.
49 * @param {Ext.util.Sorter[]} sorters The collection of Sorters being used to generate
50 * the comparator function.
51 */
52
53 /**
54 * @cfg {Number} [multiSortLimit=3]
55 * The maximum number of sorters which may be applied to this Sortable when using the "multi"
56 * insertion position when adding sorters.
57 *
58 * New sorters added using the "multi" insertion position are inserted at the top of the
59 * sorters list becoming the new primary sort key.
60 *
61 * If the sorters collection has grown to longer then **`multiSortLimit`**, then it is trimmed.
62 *
63 */
64 multiSortLimit: 3,
65
66 statics: {
67 /**
68 * Creates a single comparator function which encapsulates the passed Sorter array.
69 * @param {Ext.util.Sorter[]} sorters The sorter set for which to create a comparator
70 * function
71 * @return {Function} a function, which when passed two comparable objects returns
72 * the result of the whole sorter comparator functions.
73 */
74 createComparator: function(sorters) {
75 return sorters && sorters.length
76 ? function(r1, r2) {
77 var result = sorters[0].sort(r1, r2),
78 length = sorters.length,
79 i = 1;
80
81 // While we have not established a comparison value,
82 // loop through subsequent sorters asking for a comparison value
83 for (; !result && i < length; i++) {
84 result = sorters[i].sort.call(sorters[i], r1, r2);
85 }
86
87 return result;
88 }
89 : function() {
90 return 0;
91 };
92 }
93 },
94
95 /**
96 * @cfg {String} sortRoot
97 * The property in each item that contains the data to sort.
98 */
99
100 applySorters: function(sorters) {
101 var me = this,
102 sortersCollection;
103
104 sortersCollection = me.getSorters() || new Ext.util.MixedCollection(false, Ext.returnId);
105
106 // We have been configured with a non-default value.
107 if (sorters) {
108 sortersCollection.addAll(me.decodeSorters(sorters));
109 }
110
111 return sortersCollection;
112 },
113
114 /**
115 * Updates the sorters collection and triggers sorting of this Sortable. Example usage:
116 *
117 * //sort by a single field
118 * myStore.sort('myField', 'DESC');
119 *
120 * //sorting by multiple fields
121 * myStore.sort([{
122 * property : 'age',
123 * direction: 'ASC'
124 * }, {
125 * property : 'name',
126 * direction: 'DESC'
127 * }]);
128 *
129 * Classes which use this mixin must implement a **`soSort`** method which accepts a comparator
130 * function computed from the full sorter set which performs the sort
131 * in an implementation-specific way.
132 *
133 * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field,
134 * so this code:
135 *
136 * store.sort('myField');
137 * store.sort('myField');
138 *
139 * Is equivalent to this code, because Store handles the toggling automatically:
140 *
141 * store.sort('myField', 'ASC');
142 * store.sort('myField', 'DESC');
143 *
144 * @param {String/Ext.util.Sorter[]} [sorters] Either a string name of one of the fields
145 * in this Store's configured {@link Ext.data.Model Model}, or an array of sorter
146 * configurations.
147 * @param {String} [direction="ASC"] The overall direction to sort the data by.
148 * @param {String} [insertionPosition="replace"] Where to put the new sorter in the collection
149 * of sorters. This may take the following values:
150 *
151 * * `replace`: This means that the new sorter(s) becomes the sole sorter set for this Sortable.
152 * This is the most useful call mode to programatically sort by multiple fields.
153 *
154 * * `prepend`: This means that the new sorters are inserted as the primary sorters, unchanged,
155 * and the sorter list length must be controlled by the developer.
156 *
157 * * `multi`: This is mainly useful for implementing intuitive "Sort by this" user interfaces
158 * such as the {@link Ext.grid.Panel GridPanel}'s column sorting UI. This mode is only
159 * supported when passing a property name and a direction. This means that the new sorter
160 * becomes the primary sorter. If the sorter was **already** the primary sorter, the direction
161 * of sort is toggled if no direction parameter is specified. The number of sorters maintained
162 * is limited by the {@link #multiSortLimit} configuration.
163 *
164 * * `append` : This means that the new sorter becomes the last sorter.
165 * @param {Boolean} doSort True to sort using a generated sorter function that combines all
166 * of the Sorters passed
167 * @return {Ext.util.Sorter[]} The new sorters.
168 */
169 sort: function(sorters, direction, insertionPosition, doSort) {
170 var me = this,
171 sorter,
172 overFlow,
173 currentSorters = me.getSorters();
174
175 if (!currentSorters) {
176 me.setSorters(null);
177 currentSorters = me.getSorters();
178 }
179
180 if (Ext.isArray(sorters)) {
181 doSort = insertionPosition;
182 insertionPosition = direction;
183 }
184 else if (Ext.isObject(sorters)) {
185 sorters = [sorters];
186 doSort = insertionPosition;
187 insertionPosition = direction;
188 }
189 else if (Ext.isString(sorters)) {
190 sorter = currentSorters.get(sorters);
191
192 if (!sorter) {
193 sorter = {
194 property: sorters,
195 direction: direction
196 };
197 }
198 else if (direction == null) {
199 sorter.toggle();
200 }
201 else {
202 sorter.setDirection(direction);
203 }
204
205 sorters = [sorter];
206 }
207
208 if (sorters && sorters.length) {
209 sorters = me.decodeSorters(sorters);
210
211 switch (insertionPosition) {
212 // multi sorting means always inserting the specified sorters
213 // at the top.
214 // If we are asked to sort by what is already the primary sorter
215 // then toggle its direction.
216 case "multi":
217 // Insert the new sorter at the beginning.
218 currentSorters.insert(0, sorters[0]);
219
220 // If we now are oversize, trim our sorters collection
221 overFlow = currentSorters.getCount() - me.multiSortLimit;
222
223 if (overFlow > 0) {
224 currentSorters.removeRange(me.multiSortLimit, overFlow);
225 }
226
227 break;
228
229 case "prepend":
230 currentSorters.insert(0, sorters);
231 break;
232
233 case "append":
234 currentSorters.addAll(sorters);
235 break;
236
237 case undefined:
238 case null:
239 case "replace":
240 currentSorters.clear();
241 currentSorters.addAll(sorters);
242 break;
243
244 default:
245 //<debug>
246 Ext.raise('Sorter insertion point must be "multi", "prepend", ' +
247 '"append" or "replace"');
248 //</debug>
249 }
250 }
251
252 if (doSort !== false) {
253 me.fireEvent('beforesort', me, sorters);
254 me.onBeforeSort(sorters);
255
256 if (me.getSorterCount()) {
257 // Sort using a generated sorter function which combines all of the Sorters passed
258 me.doSort(me.generateComparator());
259 }
260 }
261
262 return sorters;
263 },
264
265 /**
266 * @protected
267 * Returns the number of Sorters which apply to this Sortable.
268 *
269 * May be overridden in subclasses. {@link Ext.data.Store Store} in particlar overrides
270 * this because its groupers must contribute to the sorter count so that the sort method above
271 * executes doSort.
272 */
273 getSorterCount: function() {
274 return this.getSorters().items.length;
275 },
276
277 /**
278 * Returns a comparator function which compares two items and returns -1, 0, or 1 depending
279 * on the currently defined set of {@link #cfg-sorters}.
280 *
281 * If there are no {@link #cfg-sorters} defined, it returns a function which returns `0` meaning
282 * that no sorting will occur.
283 */
284 generateComparator: function() {
285 var sorters = this.getSorters().getRange();
286
287 return sorters.length ? this.createComparator(sorters) : this.emptyComparator;
288 },
289
290 emptyComparator: function() {
291 return 0;
292 },
293
294 onBeforeSort: Ext.emptyFn,
295
296 /**
297 * @private
298 * Normalizes an array of sorter objects, ensuring that they are all Ext.util.Sorter instances
299 * @param {Object[]} sorters The sorters array
300 * @return {Ext.util.Sorter[]} Array of Ext.util.Sorter objects
301 */
302 decodeSorters: function(sorters) {
303 if (!Ext.isArray(sorters)) {
304 if (sorters === undefined) {
305 sorters = [];
306 }
307 else {
308 sorters = [sorters];
309 }
310 }
311
312 // eslint-disable-next-line vars-on-top
313 var length = sorters.length,
314 Sorter = Ext.util.Sorter,
315 model = this.getModel ? this.getModel() : this.model,
316 field,
317 config, i;
318
319 for (i = 0; i < length; i++) {
320 config = sorters[i];
321
322 if (!(config instanceof Sorter)) {
323 if (Ext.isString(config)) {
324 config = {
325 property: config
326 };
327 }
328
329 Ext.applyIf(config, {
330 root: this.sortRoot,
331 direction: "ASC"
332 });
333
334 // support for 3.x style sorters where a function can be defined as 'fn'
335 if (config.fn) {
336 config.sorterFn = config.fn;
337 }
338
339 // support a function to be passed as a sorter definition
340 if (typeof config === 'function') {
341 config = {
342 sorterFn: config
343 };
344 }
345
346 // ensure sortType gets pushed on if necessary
347 if (model && !config.transform) {
348 field = model.getField(config.property);
349 config.transform = field && field.sortType !== Ext.identityFn
350 ? field.sortType
351 : undefined;
352 }
353
354 sorters[i] = new Ext.util.Sorter(config);
355 }
356 }
357
358 return sorters;
359 },
360
361 /**
362 * Gets the first sorter from the sorters collection, excluding
363 * any groupers that may be in place
364 * @protected
365 * @return {Ext.util.Sorter} The sorter, null if none exist
366 */
367 getFirstSorter: function() {
368 var sorters = this.getSorters().items,
369 len = sorters.length,
370 i = 0,
371 sorter;
372
373 for (; i < len; ++i) {
374 sorter = sorters[i];
375
376 if (!sorter.isGrouper) {
377 return sorter;
378 }
379 }
380
381 return null;
382 }
383 }, function() {
384 // Reference the static implementation in prototype
385 this.prototype.createComparator = this.createComparator;
386 });