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