Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / selection / Model.js
1 /**
2  * @class Ext.selection.Model
3  * @extends Ext.util.Observable
4  *
5  * Tracks what records are currently selected in a databound widget.
6  *
7  * This is an abstract class and is not meant to be directly used.
8  *
9  * DataBound UI widgets such as GridPanel, TreePanel, and ListView
10  * should subclass AbstractStoreSelectionModel and provide a way
11  * to binding to the component.
12  *
13  * The abstract methods onSelectChange and onLastFocusChanged should
14  * be implemented in these subclasses to update the UI widget.
15  */
16 Ext.define('Ext.selection.Model', {
17     extend: 'Ext.util.Observable',
18     alternateClassName: 'Ext.AbstractStoreSelectionModel',
19     requires: ['Ext.data.StoreManager'],
20     // lastSelected
21
22     /**
23      * @cfg {String} mode
24      * Modes of selection.
25      * Valid values are SINGLE, SIMPLE, and MULTI. Defaults to 'SINGLE'
26      */
27     
28     /**
29      * @cfg {Boolean} allowDeselect
30      * Allow users to deselect a record in a DataView, List or Grid. Only applicable when the SelectionModel's mode is 'SINGLE'. Defaults to false.
31      */
32     allowDeselect: false,
33
34     /**
35      * @property selected
36      * READ-ONLY A MixedCollection that maintains all of the currently selected
37      * records.
38      */
39     selected: null,
40     
41     
42     /**
43      * Prune records when they are removed from the store from the selection.
44      * This is a private flag. For an example of its usage, take a look at
45      * Ext.selection.TreeModel.
46      * @private
47      */
48     pruneRemoved: true,
49
50     constructor: function(cfg) {
51         var me = this;
52         
53         cfg = cfg || {};
54         Ext.apply(me, cfg);
55         
56         me.addEvents(
57             /**
58              * @event selectionchange
59              * Fired after a selection change has occurred
60              * @param {Ext.selection.Model} this
61              * @param  {Array} selected The selected records
62              */
63              'selectionchange'
64         );
65
66         me.modes = {
67             SINGLE: true,
68             SIMPLE: true,
69             MULTI: true
70         };
71
72         // sets this.selectionMode
73         me.setSelectionMode(cfg.mode || me.mode);
74
75         // maintains the currently selected records.
76         me.selected = Ext.create('Ext.util.MixedCollection');
77         
78         me.callParent(arguments);
79     },
80
81     // binds the store to the selModel.
82     bind : function(store, initial){
83         var me = this;
84         
85         if(!initial && me.store){
86             if(store !== me.store && me.store.autoDestroy){
87                 me.store.destroy();
88             }else{
89                 me.store.un("add", me.onStoreAdd, me);
90                 me.store.un("clear", me.onStoreClear, me);
91                 me.store.un("remove", me.onStoreRemove, me);
92                 me.store.un("update", me.onStoreUpdate, me);
93             }
94         }
95         if(store){
96             store = Ext.data.StoreManager.lookup(store);
97             store.on({
98                 add: me.onStoreAdd,
99                 clear: me.onStoreClear,
100                 remove: me.onStoreRemove,
101                 update: me.onStoreUpdate,
102                 scope: me
103             });
104         }
105         me.store = store;
106         if(store && !initial) {
107             me.refresh();
108         }
109     },
110
111     /**
112      * Select all records in the view.
113      * @param {Boolean} suppressEvent True to suppress any selects event
114      */
115     selectAll: function(suppressEvent) {
116         var me = this,
117             selections = me.store.getRange(),
118             i = 0,
119             len = selections.length,
120             start = me.getSelection().length;
121             
122         me.bulkChange = true;
123         for (; i < len; i++) {
124             me.doSelect(selections[i], true, suppressEvent);
125         }
126         delete me.bulkChange;
127         // fire selection change only if the number of selections differs
128         me.maybeFireSelectionChange(me.getSelection().length !== start);
129     },
130
131     /**
132      * Deselect all records in the view.
133      * @param {Boolean} suppressEvent True to suppress any deselect events
134      */
135     deselectAll: function(suppressEvent) {
136         var me = this,
137             selections = me.getSelection(),
138             i = 0,
139             len = selections.length,
140             start = me.getSelection().length;
141             
142         me.bulkChange = true;
143         for (; i < len; i++) {
144             me.doDeselect(selections[i], suppressEvent);
145         }
146         delete me.bulkChange;
147         // fire selection change only if the number of selections differs
148         me.maybeFireSelectionChange(me.getSelection().length !== start);
149     },
150
151     // Provides differentiation of logic between MULTI, SIMPLE and SINGLE
152     // selection modes. Requires that an event be passed so that we can know
153     // if user held ctrl or shift.
154     selectWithEvent: function(record, e) {
155         var me = this;
156         
157         switch (me.selectionMode) {
158             case 'MULTI':
159                 if (e.ctrlKey && me.isSelected(record)) {
160                     me.doDeselect(record, false);
161                 } else if (e.shiftKey && me.lastFocused) {
162                     me.selectRange(me.lastFocused, record, e.ctrlKey);
163                 } else if (e.ctrlKey) {
164                     me.doSelect(record, true, false);
165                 } else if (me.isSelected(record) && !e.shiftKey && !e.ctrlKey && me.selected.getCount() > 1) {
166                     me.doSelect(record, false, false);
167                 } else {
168                     me.doSelect(record, false);
169                 }
170                 break;
171             case 'SIMPLE':
172                 if (me.isSelected(record)) {
173                     me.doDeselect(record);
174                 } else {
175                     me.doSelect(record, true);
176                 }
177                 break;
178             case 'SINGLE':
179                 // if allowDeselect is on and this record isSelected, deselect it
180                 if (me.allowDeselect && me.isSelected(record)) {
181                     me.doDeselect(record);
182                 // select the record and do NOT maintain existing selections
183                 } else {
184                     me.doSelect(record, false);
185                 }
186                 break;
187         }
188     },
189
190     /**
191      * Selects a range of rows if the selection model {@link #isLocked is not locked}.
192      * All rows in between startRow and endRow are also selected.
193      * @param {Ext.data.Model/Number} startRow The record or index of the first row in the range
194      * @param {Ext.data.Model/Number} endRow The record or index of the last row in the range
195      * @param {Boolean} keepExisting (optional) True to retain existing selections
196      */
197     selectRange : function(startRow, endRow, keepExisting, dir){
198         var me = this,
199             store = me.store,
200             selectedCount = 0,
201             i,
202             tmp,
203             dontDeselect,
204             records = [];
205         
206         if (me.isLocked()){
207             return;
208         }
209         
210         if (!keepExisting) {
211             me.clearSelections();
212         }
213         
214         if (!Ext.isNumber(startRow)) {
215             startRow = store.indexOf(startRow);
216         } 
217         if (!Ext.isNumber(endRow)) {
218             endRow = store.indexOf(endRow);
219         }
220         
221         // swap values
222         if (startRow > endRow){
223             tmp = endRow;
224             endRow = startRow;
225             startRow = tmp;
226         }
227
228         for (i = startRow; i <= endRow; i++) {
229             if (me.isSelected(store.getAt(i))) {
230                 selectedCount++;
231             }
232         }
233
234         if (!dir) {
235             dontDeselect = -1;
236         } else {
237             dontDeselect = (dir == 'up') ? startRow : endRow;
238         }
239         
240         for (i = startRow; i <= endRow; i++){
241             if (selectedCount == (endRow - startRow + 1)) {
242                 if (i != dontDeselect) {
243                     me.doDeselect(i, true);
244                 }
245             } else {
246                 records.push(store.getAt(i));
247             }
248         }
249         me.doMultiSelect(records, true);
250     },
251     
252     /**
253      * Selects a record instance by record instance or index.
254      * @param {Ext.data.Model/Index} records An array of records or an index
255      * @param {Boolean} keepExisting
256      * @param {Boolean} suppressEvent Set to false to not fire a select event
257      */
258     select: function(records, keepExisting, suppressEvent) {
259         this.doSelect(records, keepExisting, suppressEvent);
260     },
261
262     /**
263      * Deselects a record instance by record instance or index.
264      * @param {Ext.data.Model/Index} records An array of records or an index
265      * @param {Boolean} suppressEvent Set to false to not fire a deselect event
266      */
267     deselect: function(records, suppressEvent) {
268         this.doDeselect(records, suppressEvent);
269     },
270     
271     doSelect: function(records, keepExisting, suppressEvent) {
272         var me = this,
273             record;
274             
275         if (me.locked) {
276             return;
277         }
278         if (typeof records === "number") {
279             records = [me.store.getAt(records)];
280         }
281         if (me.selectionMode == "SINGLE" && records) {
282             record = records.length ? records[0] : records;
283             me.doSingleSelect(record, suppressEvent);
284         } else {
285             me.doMultiSelect(records, keepExisting, suppressEvent);
286         }
287     },
288
289     doMultiSelect: function(records, keepExisting, suppressEvent) {
290         var me = this,
291             selected = me.selected,
292             change = false,
293             i = 0,
294             len, record;
295             
296         if (me.locked) {
297             return;
298         }
299         
300
301         records = !Ext.isArray(records) ? [records] : records;
302         len = records.length;
303         if (!keepExisting && selected.getCount() > 0) {
304             change = true;
305             me.doDeselect(me.getSelection(), suppressEvent);
306         }
307
308         for (; i < len; i++) {
309             record = records[i];
310             if (keepExisting && me.isSelected(record)) {
311                 continue;
312             }
313             change = true;
314             me.lastSelected = record;
315             selected.add(record);
316
317             me.onSelectChange(record, true, suppressEvent);
318         }
319         me.setLastFocused(record, suppressEvent);
320         // fire selchange if there was a change and there is no suppressEvent flag
321         me.maybeFireSelectionChange(change && !suppressEvent);
322     },
323
324     // records can be an index, a record or an array of records
325     doDeselect: function(records, suppressEvent) {
326         var me = this,
327             selected = me.selected,
328             change = false,
329             i = 0,
330             len, record;
331             
332         if (me.locked) {
333             return;
334         }
335
336         if (typeof records === "number") {
337             records = [me.store.getAt(records)];
338         }
339
340         records = !Ext.isArray(records) ? [records] : records;
341         len = records.length;
342         for (; i < len; i++) {
343             record = records[i];
344             if (selected.remove(record)) {
345                 if (me.lastSelected == record) {
346                     me.lastSelected = selected.last();
347                 }
348                 me.onSelectChange(record, false, suppressEvent);
349                 change = true;
350             }
351         }
352         // fire selchange if there was a change and there is no suppressEvent flag
353         me.maybeFireSelectionChange(change && !suppressEvent);
354     },
355
356     doSingleSelect: function(record, suppressEvent) {
357         var me = this,
358             selected = me.selected;
359             
360         if (me.locked) {
361             return;
362         }
363         // already selected.
364         // should we also check beforeselect?
365         if (me.isSelected(record)) {
366             return;
367         }
368         if (selected.getCount() > 0) {
369             me.doDeselect(me.lastSelected, suppressEvent);
370         }
371         selected.add(record);
372         me.lastSelected = record;
373         me.onSelectChange(record, true, suppressEvent);
374         if (!suppressEvent) {
375             me.setLastFocused(record);
376         }
377         me.maybeFireSelectionChange(!suppressEvent);
378     },
379
380     /**
381      * @param {Ext.data.Model} record
382      * Set a record as the last focused record. This does NOT mean
383      * that the record has been selected.
384      */
385     setLastFocused: function(record, supressFocus) {
386         var me = this,
387             recordBeforeLast = me.lastFocused;
388         me.lastFocused = record;
389         me.onLastFocusChanged(recordBeforeLast, record, supressFocus);
390     },
391     
392     /**
393      * Determines if this record is currently focused.
394      * @param Ext.data.Record record
395      */
396     isFocused: function(record) {
397         return record === this.getLastFocused();
398     },
399
400
401     // fire selection change as long as true is not passed
402     // into maybeFireSelectionChange
403     maybeFireSelectionChange: function(fireEvent) {
404         var me = this;
405         if (fireEvent && !me.bulkChange) {
406             me.fireEvent('selectionchange', me, me.getSelection());
407         }
408     },
409
410     /**
411      * Returns the last selected record.
412      */
413     getLastSelected: function() {
414         return this.lastSelected;
415     },
416     
417     getLastFocused: function() {
418         return this.lastFocused;
419     },
420
421     /**
422      * Returns an array of the currently selected records.
423      */
424     getSelection: function() {
425         return this.selected.getRange();
426     },
427
428     /**
429      * Returns the current selectionMode. SINGLE, MULTI or SIMPLE.
430      */
431     getSelectionMode: function() {
432         return this.selectionMode;
433     },
434
435     /**
436      * Sets the current selectionMode. SINGLE, MULTI or SIMPLE.
437      */
438     setSelectionMode: function(selMode) {
439         selMode = selMode ? selMode.toUpperCase() : 'SINGLE';
440         // set to mode specified unless it doesnt exist, in that case
441         // use single.
442         this.selectionMode = this.modes[selMode] ? selMode : 'SINGLE';
443     },
444
445     /**
446      * Returns true if the selections are locked.
447      * @return {Boolean}
448      */
449     isLocked: function() {
450         return this.locked;
451     },
452
453     /**
454      * Locks the current selection and disables any changes from
455      * happening to the selection.
456      * @param {Boolean} locked
457      */
458     setLocked: function(locked) {
459         this.locked = !!locked;
460     },
461
462     /**
463      * Returns <tt>true</tt> if the specified row is selected.
464      * @param {Record/Number} record The record or index of the record to check
465      * @return {Boolean}
466      */
467     isSelected: function(record) {
468         record = Ext.isNumber(record) ? this.store.getAt(record) : record;
469         return this.selected.indexOf(record) !== -1;
470     },
471     
472     /**
473      * Returns true if there is a selected record.
474      * @return {Boolean}
475      */
476     hasSelection: function() {
477         return this.selected.getCount() > 0;
478     },
479
480     refresh: function() {
481         var me = this,
482             toBeSelected = [],
483             oldSelections = me.getSelection(),
484             len = oldSelections.length,
485             selection,
486             change,
487             i = 0,
488             lastFocused = this.getLastFocused();
489
490         // check to make sure that there are no records
491         // missing after the refresh was triggered, prune
492         // them from what is to be selected if so
493         for (; i < len; i++) {
494             selection = oldSelections[i];
495             if (!this.pruneRemoved || me.store.indexOf(selection) !== -1) {
496                 toBeSelected.push(selection);
497             }
498         }
499
500         // there was a change from the old selected and
501         // the new selection
502         if (me.selected.getCount() != toBeSelected.length) {
503             change = true;
504         }
505
506         me.clearSelections();
507         
508         if (me.store.indexOf(lastFocused) !== -1) {
509             // restore the last focus but supress restoring focus
510             this.setLastFocused(lastFocused, true);
511         }
512
513         if (toBeSelected.length) {
514             // perform the selection again
515             me.doSelect(toBeSelected, false, true);
516         }
517         
518         me.maybeFireSelectionChange(change);
519     },
520
521     /**
522      * A fast reset of the selections without firing events, updating the ui, etc.
523      * For private usage only.
524      * @private
525      */
526     clearSelections: function() {
527         // reset the entire selection to nothing
528         var me = this;
529         me.selected.clear();
530         me.lastSelected = null;
531         me.setLastFocused(null);
532     },
533
534     // when a record is added to a store
535     onStoreAdd: function() {
536
537     },
538
539     // when a store is cleared remove all selections
540     // (if there were any)
541     onStoreClear: function() {
542         var me = this,
543             selected = this.selected;
544             
545         if (selected.getCount > 0) {
546             selected.clear();
547             me.lastSelected = null;
548             me.setLastFocused(null);
549             me.maybeFireSelectionChange(true);
550         }
551     },
552
553     // prune records from the SelectionModel if
554     // they were selected at the time they were
555     // removed.
556     onStoreRemove: function(store, record) {
557         var me = this,
558             selected = me.selected;
559             
560         if (me.locked || !me.pruneRemoved) {
561             return;
562         }
563
564         if (selected.remove(record)) {
565             if (me.lastSelected == record) {
566                 me.lastSelected = null;
567             }
568             if (me.getLastFocused() == record) {
569                 me.setLastFocused(null);
570             }
571             me.maybeFireSelectionChange(true);
572         }
573     },
574
575     getCount: function() {
576         return this.selected.getCount();
577     },
578
579     // cleanup.
580     destroy: function() {
581
582     },
583
584     // if records are updated
585     onStoreUpdate: function() {
586
587     },
588
589     // @abstract
590     onSelectChange: function(record, isSelected, suppressEvent) {
591
592     },
593
594     // @abstract
595     onLastFocusChanged: function(oldFocused, newFocused) {
596
597     },
598
599     // @abstract
600     onEditorKey: function(field, e) {
601
602     },
603
604     // @abstract
605     bindComponent: function(cmp) {
606
607     }
608 });