Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / selection / RowModel.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.selection.RowModel
17  * @extends Ext.selection.Model
18  */
19 Ext.define('Ext.selection.RowModel', {
20     extend: 'Ext.selection.Model',
21     alias: 'selection.rowmodel',
22     requires: ['Ext.util.KeyNav'],
23
24     /**
25      * @private
26      * Number of pixels to scroll to the left/right when pressing
27      * left/right keys.
28      */
29     deltaScroll: 5,
30
31     /**
32      * @cfg {Boolean} enableKeyNav
33      *
34      * Turns on/off keyboard navigation within the grid.
35      */
36     enableKeyNav: true,
37     
38     /**
39      * @cfg {Boolean} [ignoreRightMouseSelection=true]
40      * True to ignore selections that are made when using the right mouse button if there are
41      * records that are already selected. If no records are selected, selection will continue 
42      * as normal
43      */
44     ignoreRightMouseSelection: true,
45
46     constructor: function(){
47         this.addEvents(
48             /**
49              * @event beforedeselect
50              * Fired before a record is deselected. If any listener returns false, the
51              * deselection is cancelled.
52              * @param {Ext.selection.RowModel} this
53              * @param {Ext.data.Model} record The deselected record
54              * @param {Number} index The row index deselected
55              */
56             'beforedeselect',
57
58             /**
59              * @event beforeselect
60              * Fired before a record is selected. If any listener returns false, the
61              * selection is cancelled.
62              * @param {Ext.selection.RowModel} this
63              * @param {Ext.data.Model} record The selected record
64              * @param {Number} index The row index selected
65              */
66             'beforeselect',
67
68             /**
69              * @event deselect
70              * Fired after a record is deselected
71              * @param {Ext.selection.RowModel} this
72              * @param {Ext.data.Model} record The deselected record
73              * @param {Number} index The row index deselected
74              */
75             'deselect',
76
77             /**
78              * @event select
79              * Fired after a record is selected
80              * @param {Ext.selection.RowModel} this
81              * @param {Ext.data.Model} record The selected record
82              * @param {Number} index The row index selected
83              */
84             'select'
85         );
86         this.callParent(arguments);
87     },
88
89     bindComponent: function(view) {
90         var me = this;
91
92         me.views = me.views || [];
93         me.views.push(view);
94         me.bind(view.getStore(), true);
95
96         view.on({
97             itemmousedown: me.onRowMouseDown,
98             scope: me
99         });
100
101         if (me.enableKeyNav) {
102             me.initKeyNav(view);
103         }
104     },
105
106     initKeyNav: function(view) {
107         var me = this;
108
109         if (!view.rendered) {
110             view.on('render', Ext.Function.bind(me.initKeyNav, me, [view], 0), me, {single: true});
111             return;
112         }
113
114         view.el.set({
115             tabIndex: -1
116         });
117
118         // view.el has tabIndex -1 to allow for
119         // keyboard events to be passed to it.
120         me.keyNav = new Ext.util.KeyNav(view.el, {
121             up: me.onKeyUp,
122             down: me.onKeyDown,
123             right: me.onKeyRight,
124             left: me.onKeyLeft,
125             pageDown: me.onKeyPageDown,
126             pageUp: me.onKeyPageUp,
127             home: me.onKeyHome,
128             end: me.onKeyEnd,
129             scope: me
130         });
131         view.el.on(Ext.EventManager.getKeyEvent(), me.onKeyPress, me);
132     },
133
134     // Returns the number of rows currently visible on the screen or
135     // false if there were no rows. This assumes that all rows are
136     // of the same height and the first view is accurate.
137     getRowsVisible: function() {
138         var rowsVisible = false,
139             view = this.views[0],
140             row = view.getNode(0),
141             rowHeight, gridViewHeight;
142
143         if (row) {
144             rowHeight = Ext.fly(row).getHeight();
145             gridViewHeight = view.el.getHeight();
146             rowsVisible = Math.floor(gridViewHeight / rowHeight);
147         }
148
149         return rowsVisible;
150     },
151
152     // go to last visible record in grid.
153     onKeyEnd: function(e, t) {
154         var me = this,
155             last = me.store.getAt(me.store.getCount() - 1);
156
157         if (last) {
158             if (e.shiftKey) {
159                 me.selectRange(last, me.lastFocused || 0);
160                 me.setLastFocused(last);
161             } else if (e.ctrlKey) {
162                 me.setLastFocused(last);
163             } else {
164                 me.doSelect(last);
165             }
166         }
167     },
168
169     // go to first visible record in grid.
170     onKeyHome: function(e, t) {
171         var me = this,
172             first = me.store.getAt(0);
173
174         if (first) {
175             if (e.shiftKey) {
176                 me.selectRange(first, me.lastFocused || 0);
177                 me.setLastFocused(first);
178             } else if (e.ctrlKey) {
179                 me.setLastFocused(first);
180             } else {
181                 me.doSelect(first, false);
182             }
183         }
184     },
185
186     // Go one page up from the lastFocused record in the grid.
187     onKeyPageUp: function(e, t) {
188         var me = this,
189             rowsVisible = me.getRowsVisible(),
190             selIdx,
191             prevIdx,
192             prevRecord,
193             currRec;
194
195         if (rowsVisible) {
196             selIdx = me.lastFocused ? me.store.indexOf(me.lastFocused) : 0;
197             prevIdx = selIdx - rowsVisible;
198             if (prevIdx < 0) {
199                 prevIdx = 0;
200             }
201             prevRecord = me.store.getAt(prevIdx);
202             if (e.shiftKey) {
203                 currRec = me.store.getAt(selIdx);
204                 me.selectRange(prevRecord, currRec, e.ctrlKey, 'up');
205                 me.setLastFocused(prevRecord);
206             } else if (e.ctrlKey) {
207                 e.preventDefault();
208                 me.setLastFocused(prevRecord);
209             } else {
210                 me.doSelect(prevRecord);
211             }
212
213         }
214     },
215
216     // Go one page down from the lastFocused record in the grid.
217     onKeyPageDown: function(e, t) {
218         var me = this,
219             rowsVisible = me.getRowsVisible(),
220             selIdx,
221             nextIdx,
222             nextRecord,
223             currRec;
224
225         if (rowsVisible) {
226             selIdx = me.lastFocused ? me.store.indexOf(me.lastFocused) : 0;
227             nextIdx = selIdx + rowsVisible;
228             if (nextIdx >= me.store.getCount()) {
229                 nextIdx = me.store.getCount() - 1;
230             }
231             nextRecord = me.store.getAt(nextIdx);
232             if (e.shiftKey) {
233                 currRec = me.store.getAt(selIdx);
234                 me.selectRange(nextRecord, currRec, e.ctrlKey, 'down');
235                 me.setLastFocused(nextRecord);
236             } else if (e.ctrlKey) {
237                 // some browsers, this means go thru browser tabs
238                 // attempt to stop.
239                 e.preventDefault();
240                 me.setLastFocused(nextRecord);
241             } else {
242                 me.doSelect(nextRecord);
243             }
244         }
245     },
246
247     // Select/Deselect based on pressing Spacebar.
248     // Assumes a SIMPLE selectionmode style
249     onKeyPress: function(e, t) {
250         if (e.getKey() === e.SPACE) {
251             e.stopEvent();
252             var me = this,
253                 record = me.lastFocused;
254
255             if (record) {
256                 if (me.isSelected(record)) {
257                     me.doDeselect(record, false);
258                 } else {
259                     me.doSelect(record, true);
260                 }
261             }
262         }
263     },
264
265     // Navigate one record up. This could be a selection or
266     // could be simply focusing a record for discontiguous
267     // selection. Provides bounds checking.
268     onKeyUp: function(e, t) {
269         var me = this,
270             view = me.views[0],
271             idx  = me.store.indexOf(me.lastFocused),
272             record;
273
274         if (idx > 0) {
275             // needs to be the filtered count as thats what
276             // will be visible.
277             record = me.store.getAt(idx - 1);
278             if (e.shiftKey && me.lastFocused) {
279                 if (me.isSelected(me.lastFocused) && me.isSelected(record)) {
280                     me.doDeselect(me.lastFocused, true);
281                     me.setLastFocused(record);
282                 } else if (!me.isSelected(me.lastFocused)) {
283                     me.doSelect(me.lastFocused, true);
284                     me.doSelect(record, true);
285                 } else {
286                     me.doSelect(record, true);
287                 }
288             } else if (e.ctrlKey) {
289                 me.setLastFocused(record);
290             } else {
291                 me.doSelect(record);
292                 //view.focusRow(idx - 1);
293             }
294         }
295         // There was no lastFocused record, and the user has pressed up
296         // Ignore??
297         //else if (this.selected.getCount() == 0) {
298         //
299         //    this.doSelect(record);
300         //    //view.focusRow(idx - 1);
301         //}
302     },
303
304     // Navigate one record down. This could be a selection or
305     // could be simply focusing a record for discontiguous
306     // selection. Provides bounds checking.
307     onKeyDown: function(e, t) {
308         var me = this,
309             view = me.views[0],
310             idx  = me.store.indexOf(me.lastFocused),
311             record;
312
313         // needs to be the filtered count as thats what
314         // will be visible.
315         if (idx + 1 < me.store.getCount()) {
316             record = me.store.getAt(idx + 1);
317             if (me.selected.getCount() === 0) {
318                 me.doSelect(record);
319                 //view.focusRow(idx + 1);
320             } else if (e.shiftKey && me.lastFocused) {
321                 if (me.isSelected(me.lastFocused) && me.isSelected(record)) {
322                     me.doDeselect(me.lastFocused, true);
323                     me.setLastFocused(record);
324                 } else if (!me.isSelected(me.lastFocused)) {
325                     me.doSelect(me.lastFocused, true);
326                     me.doSelect(record, true);
327                 } else {
328                     me.doSelect(record, true);
329                 }
330             } else if (e.ctrlKey) {
331                 me.setLastFocused(record);
332             } else {
333                 me.doSelect(record);
334                 //view.focusRow(idx + 1);
335             }
336         }
337     },
338
339     scrollByDeltaX: function(delta) {
340         var view    = this.views[0],
341             section = view.up(),
342             hScroll = section.horizontalScroller;
343
344         if (hScroll) {
345             hScroll.scrollByDeltaX(delta);
346         }
347     },
348
349     onKeyLeft: function(e, t) {
350         this.scrollByDeltaX(-this.deltaScroll);
351     },
352
353     onKeyRight: function(e, t) {
354         this.scrollByDeltaX(this.deltaScroll);
355     },
356
357     // Select the record with the event included so that
358     // we can take into account ctrlKey, shiftKey, etc
359     onRowMouseDown: function(view, record, item, index, e) {
360         view.el.focus();
361         if (!this.allowRightMouseSelection(e)) {
362             return;
363         }
364         this.selectWithEvent(record, e);
365     },
366     
367     /**
368      * Checks whether a selection should proceed based on the ignoreRightMouseSelection
369      * option.
370      * @private
371      * @param {Ext.EventObject} e The event
372      * @return {Boolean} False if the selection should not proceed
373      */
374     allowRightMouseSelection: function(e) {
375         var disallow = this.ignoreRightMouseSelection && e.button !== 0;
376         if (disallow) {
377             disallow = this.hasSelection();
378         }
379         return !disallow;
380     },
381
382     // Allow the GridView to update the UI by
383     // adding/removing a CSS class from the row.
384     onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
385         var me      = this,
386             views   = me.views,
387             viewsLn = views.length,
388             store   = me.store,
389             rowIdx  = store.indexOf(record),
390             eventName = isSelected ? 'select' : 'deselect',
391             i = 0;
392
393         if ((suppressEvent || me.fireEvent('before' + eventName, me, record, rowIdx)) !== false &&
394                 commitFn() !== false) {
395
396             for (; i < viewsLn; i++) {
397                 if (isSelected) {
398                     views[i].onRowSelect(rowIdx, suppressEvent);
399                 } else {
400                     views[i].onRowDeselect(rowIdx, suppressEvent);
401                 }
402             }
403
404             if (!suppressEvent) {
405                 me.fireEvent(eventName, me, record, rowIdx);
406             }
407         }
408     },
409
410     // Provide indication of what row was last focused via
411     // the gridview.
412     onLastFocusChanged: function(oldFocused, newFocused, supressFocus) {
413         var views   = this.views,
414             viewsLn = views.length,
415             store   = this.store,
416             rowIdx,
417             i = 0;
418
419         if (oldFocused) {
420             rowIdx = store.indexOf(oldFocused);
421             if (rowIdx != -1) {
422                 for (; i < viewsLn; i++) {
423                     views[i].onRowFocus(rowIdx, false);
424                 }
425             }
426         }
427
428         if (newFocused) {
429             rowIdx = store.indexOf(newFocused);
430             if (rowIdx != -1) {
431                 for (i = 0; i < viewsLn; i++) {
432                     views[i].onRowFocus(rowIdx, true, supressFocus);
433                 }
434             }
435         }
436     },
437
438     onEditorTab: function(editingPlugin, e) {
439         var me = this,
440             view = me.views[0],
441             record = editingPlugin.getActiveRecord(),
442             header = editingPlugin.getActiveColumn(),
443             position = view.getPosition(record, header),
444             direction = e.shiftKey ? 'left' : 'right',
445             newPosition  = view.walkCells(position, direction, e, this.preventWrap);
446
447         if (newPosition) {
448             editingPlugin.startEditByPosition(newPosition);
449         }
450     },
451
452     selectByPosition: function(position) {
453         var record = this.store.getAt(position.row);
454         this.select(record);
455     }
456 });