Upgrade to ExtJS 3.0.0 - Released 07/06/2009
[extjs.git] / src / widgets / grid / RowSelectionModel.js
1 /*!
2  * Ext JS Library 3.0.0
3  * Copyright(c) 2006-2009 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.grid.RowSelectionModel
9  * @extends Ext.grid.AbstractSelectionModel
10  * The default SelectionModel used by {@link Ext.grid.GridPanel}.
11  * It supports multiple selections and keyboard selection/navigation. The objects stored
12  * as selections and returned by {@link #getSelected}, and {@link #getSelections} are
13  * the {@link Ext.data.Record Record}s which provide the data for the selected rows.
14  * @constructor
15  * @param {Object} config
16  */
17 Ext.grid.RowSelectionModel = function(config){
18     Ext.apply(this, config);
19     this.selections = new Ext.util.MixedCollection(false, function(o){
20         return o.id;
21     });
22
23     this.last = false;
24     this.lastActive = false;
25
26     this.addEvents(
27         /**
28          * @event selectionchange
29          * Fires when the selection changes
30          * @param {SelectionModel} this
31          */
32         "selectionchange",
33         /**
34          * @event beforerowselect
35          * Fires before a row is selected, return false to cancel the selection.
36          * @param {SelectionModel} this
37          * @param {Number} rowIndex The index to be selected
38          * @param {Boolean} keepExisting False if other selections will be cleared
39          * @param {Record} record The record to be selected
40          */
41         "beforerowselect",
42         /**
43          * @event rowselect
44          * Fires when a row is selected.
45          * @param {SelectionModel} this
46          * @param {Number} rowIndex The selected index
47          * @param {Ext.data.Record} r The selected record
48          */
49         "rowselect",
50         /**
51          * @event rowdeselect
52          * Fires when a row is deselected.  To prevent deselection
53          * {@link Ext.grid.AbstractSelectionModel#lock lock the selections}. 
54          * @param {SelectionModel} this
55          * @param {Number} rowIndex
56          * @param {Record} record
57          */
58         "rowdeselect"
59     );
60
61     Ext.grid.RowSelectionModel.superclass.constructor.call(this);
62 };
63
64 Ext.extend(Ext.grid.RowSelectionModel, Ext.grid.AbstractSelectionModel,  {
65     /**
66      * @cfg {Boolean} singleSelect
67      * <tt>true</tt> to allow selection of only one row at a time (defaults to <tt>false</tt>
68      * allowing multiple selections)
69      */
70     singleSelect : false,
71
72     /**
73      * @cfg {Boolean} moveEditorOnEnter
74      * <tt>false</tt> to turn off moving the editor to the next row down when the enter key is pressed
75      * or the next row up when shift + enter keys are pressed.
76      */
77     // private
78     initEvents : function(){
79
80         if(!this.grid.enableDragDrop && !this.grid.enableDrag){
81             this.grid.on("rowmousedown", this.handleMouseDown, this);
82         }else{ // allow click to work like normal
83             this.grid.on("rowclick", function(grid, rowIndex, e) {
84                 if(e.button === 0 && !e.shiftKey && !e.ctrlKey) {
85                     this.selectRow(rowIndex, false);
86                     grid.view.focusRow(rowIndex);
87                 }
88             }, this);
89         }
90
91         this.rowNav = new Ext.KeyNav(this.grid.getGridEl(), {
92             "up" : function(e){
93                 if(!e.shiftKey || this.singleSelect){
94                     this.selectPrevious(false);
95                 }else if(this.last !== false && this.lastActive !== false){
96                     var last = this.last;
97                     this.selectRange(this.last,  this.lastActive-1);
98                     this.grid.getView().focusRow(this.lastActive);
99                     if(last !== false){
100                         this.last = last;
101                     }
102                 }else{
103                     this.selectFirstRow();
104                 }
105             },
106             "down" : function(e){
107                 if(!e.shiftKey || this.singleSelect){
108                     this.selectNext(false);
109                 }else if(this.last !== false && this.lastActive !== false){
110                     var last = this.last;
111                     this.selectRange(this.last,  this.lastActive+1);
112                     this.grid.getView().focusRow(this.lastActive);
113                     if(last !== false){
114                         this.last = last;
115                     }
116                 }else{
117                     this.selectFirstRow();
118                 }
119             },
120             scope: this
121         });
122
123         var view = this.grid.view;
124         view.on("refresh", this.onRefresh, this);
125         view.on("rowupdated", this.onRowUpdated, this);
126         view.on("rowremoved", this.onRemove, this);
127     },
128
129     // private
130     onRefresh : function(){
131         var ds = this.grid.store, index;
132         var s = this.getSelections();
133         this.clearSelections(true);
134         for(var i = 0, len = s.length; i < len; i++){
135             var r = s[i];
136             if((index = ds.indexOfId(r.id)) != -1){
137                 this.selectRow(index, true);
138             }
139         }
140         if(s.length != this.selections.getCount()){
141             this.fireEvent("selectionchange", this);
142         }
143     },
144
145     // private
146     onRemove : function(v, index, r){
147         if(this.selections.remove(r) !== false){
148             this.fireEvent('selectionchange', this);
149         }
150     },
151
152     // private
153     onRowUpdated : function(v, index, r){
154         if(this.isSelected(r)){
155             v.onRowSelect(index);
156         }
157     },
158
159     /**
160      * Select records.
161      * @param {Array} records The records to select
162      * @param {Boolean} keepExisting (optional) <tt>true</tt> to keep existing selections
163      */
164     selectRecords : function(records, keepExisting){
165         if(!keepExisting){
166             this.clearSelections();
167         }
168         var ds = this.grid.store;
169         for(var i = 0, len = records.length; i < len; i++){
170             this.selectRow(ds.indexOf(records[i]), true);
171         }
172     },
173
174     /**
175      * Gets the number of selected rows.
176      * @return {Number}
177      */
178     getCount : function(){
179         return this.selections.length;
180     },
181
182     /**
183      * Selects the first row in the grid.
184      */
185     selectFirstRow : function(){
186         this.selectRow(0);
187     },
188
189     /**
190      * Select the last row.
191      * @param {Boolean} keepExisting (optional) <tt>true</tt> to keep existing selections
192      */
193     selectLastRow : function(keepExisting){
194         this.selectRow(this.grid.store.getCount() - 1, keepExisting);
195     },
196
197     /**
198      * Selects the row immediately following the last selected row.
199      * @param {Boolean} keepExisting (optional) <tt>true</tt> to keep existing selections
200      * @return {Boolean} <tt>true</tt> if there is a next row, else <tt>false</tt>
201      */
202     selectNext : function(keepExisting){
203         if(this.hasNext()){
204             this.selectRow(this.last+1, keepExisting);
205             this.grid.getView().focusRow(this.last);
206             return true;
207         }
208         return false;
209     },
210
211     /**
212      * Selects the row that precedes the last selected row.
213      * @param {Boolean} keepExisting (optional) <tt>true</tt> to keep existing selections
214      * @return {Boolean} <tt>true</tt> if there is a previous row, else <tt>false</tt>
215      */
216     selectPrevious : function(keepExisting){
217         if(this.hasPrevious()){
218             this.selectRow(this.last-1, keepExisting);
219             this.grid.getView().focusRow(this.last);
220             return true;
221         }
222         return false;
223     },
224
225     /**
226      * Returns true if there is a next record to select
227      * @return {Boolean}
228      */
229     hasNext : function(){
230         return this.last !== false && (this.last+1) < this.grid.store.getCount();
231     },
232
233     /**
234      * Returns true if there is a previous record to select
235      * @return {Boolean}
236      */
237     hasPrevious : function(){
238         return !!this.last;
239     },
240
241
242     /**
243      * Returns the selected records
244      * @return {Array} Array of selected records
245      */
246     getSelections : function(){
247         return [].concat(this.selections.items);
248     },
249
250     /**
251      * Returns the first selected record.
252      * @return {Record}
253      */
254     getSelected : function(){
255         return this.selections.itemAt(0);
256     },
257
258     /**
259      * Calls the passed function with each selection. If the function returns
260      * <tt>false</tt>, iteration is stopped and this function returns
261      * <tt>false</tt>. Otherwise it returns <tt>true</tt>.
262      * @param {Function} fn
263      * @param {Object} scope (optional)
264      * @return {Boolean} true if all selections were iterated
265      */
266     each : function(fn, scope){
267         var s = this.getSelections();
268         for(var i = 0, len = s.length; i < len; i++){
269             if(fn.call(scope || this, s[i], i) === false){
270                 return false;
271             }
272         }
273         return true;
274     },
275
276     /**
277      * Clears all selections if the selection model
278      * {@link Ext.grid.AbstractSelectionModel#isLocked is not locked}.
279      * @param {Boolean} fast (optional) <tt>true</tt> to bypass the
280      * conditional checks and events described in {@link #deselectRow}.
281      */
282     clearSelections : function(fast){
283         if(this.isLocked()){
284             return;
285         }
286         if(fast !== true){
287             var ds = this.grid.store;
288             var s = this.selections;
289             s.each(function(r){
290                 this.deselectRow(ds.indexOfId(r.id));
291             }, this);
292             s.clear();
293         }else{
294             this.selections.clear();
295         }
296         this.last = false;
297     },
298
299
300     /**
301      * Selects all rows if the selection model
302      * {@link Ext.grid.AbstractSelectionModel#isLocked is not locked}. 
303      */
304     selectAll : function(){
305         if(this.isLocked()){
306             return;
307         }
308         this.selections.clear();
309         for(var i = 0, len = this.grid.store.getCount(); i < len; i++){
310             this.selectRow(i, true);
311         }
312     },
313
314     /**
315      * Returns <tt>true</tt> if there is a selection.
316      * @return {Boolean}
317      */
318     hasSelection : function(){
319         return this.selections.length > 0;
320     },
321
322     /**
323      * Returns <tt>true</tt> if the specified row is selected.
324      * @param {Number/Record} index The record or index of the record to check
325      * @return {Boolean}
326      */
327     isSelected : function(index){
328         var r = typeof index == "number" ? this.grid.store.getAt(index) : index;
329         return (r && this.selections.key(r.id) ? true : false);
330     },
331
332     /**
333      * Returns <tt>true</tt> if the specified record id is selected.
334      * @param {String} id The id of record to check
335      * @return {Boolean}
336      */
337     isIdSelected : function(id){
338         return (this.selections.key(id) ? true : false);
339     },
340
341     // private
342     handleMouseDown : function(g, rowIndex, e){
343         if(e.button !== 0 || this.isLocked()){
344             return;
345         }
346         var view = this.grid.getView();
347         if(e.shiftKey && !this.singleSelect && this.last !== false){
348             var last = this.last;
349             this.selectRange(last, rowIndex, e.ctrlKey);
350             this.last = last; // reset the last
351             view.focusRow(rowIndex);
352         }else{
353             var isSelected = this.isSelected(rowIndex);
354             if(e.ctrlKey && isSelected){
355                 this.deselectRow(rowIndex);
356             }else if(!isSelected || this.getCount() > 1){
357                 this.selectRow(rowIndex, e.ctrlKey || e.shiftKey);
358                 view.focusRow(rowIndex);
359             }
360         }
361     },
362
363     /**
364      * Selects multiple rows.
365      * @param {Array} rows Array of the indexes of the row to select
366      * @param {Boolean} keepExisting (optional) <tt>true</tt> to keep
367      * existing selections (defaults to <tt>false</tt>)
368      */
369     selectRows : function(rows, keepExisting){
370         if(!keepExisting){
371             this.clearSelections();
372         }
373         for(var i = 0, len = rows.length; i < len; i++){
374             this.selectRow(rows[i], true);
375         }
376     },
377
378     /**
379      * Selects a range of rows if the selection model
380      * {@link Ext.grid.AbstractSelectionModel#isLocked is not locked}.
381      * All rows in between startRow and endRow are also selected.
382      * @param {Number} startRow The index of the first row in the range
383      * @param {Number} endRow The index of the last row in the range
384      * @param {Boolean} keepExisting (optional) True to retain existing selections
385      */
386     selectRange : function(startRow, endRow, keepExisting){
387         var i;
388         if(this.isLocked()){
389             return;
390         }
391         if(!keepExisting){
392             this.clearSelections();
393         }
394         if(startRow <= endRow){
395             for(i = startRow; i <= endRow; i++){
396                 this.selectRow(i, true);
397             }
398         }else{
399             for(i = startRow; i >= endRow; i--){
400                 this.selectRow(i, true);
401             }
402         }
403     },
404
405     /**
406      * Deselects a range of rows if the selection model
407      * {@link Ext.grid.AbstractSelectionModel#isLocked is not locked}.  
408      * All rows in between startRow and endRow are also deselected.
409      * @param {Number} startRow The index of the first row in the range
410      * @param {Number} endRow The index of the last row in the range
411      */
412     deselectRange : function(startRow, endRow, preventViewNotify){
413         if(this.isLocked()){
414             return;
415         }
416         for(var i = startRow; i <= endRow; i++){
417             this.deselectRow(i, preventViewNotify);
418         }
419     },
420
421     /**
422      * Selects a row.  Before selecting a row, checks if the selection model
423      * {@link Ext.grid.AbstractSelectionModel#isLocked is locked} and fires the
424      * {@link #beforerowselect} event.  If these checks are satisfied the row
425      * will be selected and followed up by  firing the {@link #rowselect} and
426      * {@link #selectionchange} events.
427      * @param {Number} row The index of the row to select
428      * @param {Boolean} keepExisting (optional) <tt>true</tt> to keep existing selections
429      * @param {Boolean} preventViewNotify (optional) Specify <tt>true</tt> to
430      * prevent notifying the view (disables updating the selected appearance)
431      */
432     selectRow : function(index, keepExisting, preventViewNotify){
433         if(this.isLocked() || (index < 0 || index >= this.grid.store.getCount()) || (keepExisting && this.isSelected(index))){
434             return;
435         }
436         var r = this.grid.store.getAt(index);
437         if(r && this.fireEvent("beforerowselect", this, index, keepExisting, r) !== false){
438             if(!keepExisting || this.singleSelect){
439                 this.clearSelections();
440             }
441             this.selections.add(r);
442             this.last = this.lastActive = index;
443             if(!preventViewNotify){
444                 this.grid.getView().onRowSelect(index);
445             }
446             this.fireEvent("rowselect", this, index, r);
447             this.fireEvent("selectionchange", this);
448         }
449     },
450
451     /**
452      * Deselects a row.  Before deselecting a row, checks if the selection model
453      * {@link Ext.grid.AbstractSelectionModel#isLocked is locked}.
454      * If this check is satisfied the row will be deselected and followed up by
455      * firing the {@link #rowdeselect} and {@link #selectionchange} events.
456      * @param {Number} row The index of the row to deselect
457      * @param {Boolean} preventViewNotify (optional) Specify <tt>true</tt> to
458      * prevent notifying the view (disables updating the selected appearance)
459      */
460     deselectRow : function(index, preventViewNotify){
461         if(this.isLocked()){
462             return;
463         }
464         if(this.last == index){
465             this.last = false;
466         }
467         if(this.lastActive == index){
468             this.lastActive = false;
469         }
470         var r = this.grid.store.getAt(index);
471         if(r){
472             this.selections.remove(r);
473             if(!preventViewNotify){
474                 this.grid.getView().onRowDeselect(index);
475             }
476             this.fireEvent("rowdeselect", this, index, r);
477             this.fireEvent("selectionchange", this);
478         }
479     },
480
481     // private
482     restoreLast : function(){
483         if(this._last){
484             this.last = this._last;
485         }
486     },
487
488     // private
489     acceptsNav : function(row, col, cm){
490         return !cm.isHidden(col) && cm.isCellEditable(col, row);
491     },
492
493     // private
494     onEditorKey : function(field, e){
495         var k = e.getKey(), newCell, g = this.grid, ed = g.activeEditor;
496         var shift = e.shiftKey;
497         if(k == e.TAB){
498             e.stopEvent();
499             ed.completeEdit();
500             if(shift){
501                 newCell = g.walkCells(ed.row, ed.col-1, -1, this.acceptsNav, this);
502             }else{
503                 newCell = g.walkCells(ed.row, ed.col+1, 1, this.acceptsNav, this);
504             }
505         }else if(k == e.ENTER){
506             e.stopEvent();
507             ed.completeEdit();
508             if(this.moveEditorOnEnter !== false){
509                 if(shift){
510                     newCell = g.walkCells(ed.row - 1, ed.col, -1, this.acceptsNav, this);
511                 }else{
512                     newCell = g.walkCells(ed.row + 1, ed.col, 1, this.acceptsNav, this);
513                 }
514             }
515         }else if(k == e.ESC){
516             ed.cancelEdit();
517         }
518         if(newCell){
519             g.startEditing(newCell[0], newCell[1]);
520         }
521     },
522     
523     destroy: function(){
524         if(this.rowNav){
525             this.rowNav.disable();
526             this.rowNav = null;
527         }
528         Ext.grid.RowSelectionModel.superclass.destroy.call(this);
529     }
530 });