3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.selection.Model
17 * @extends Ext.util.Observable
19 * Tracks what records are currently selected in a databound widget.
21 * This is an abstract class and is not meant to be directly used.
23 * DataBound UI widgets such as GridPanel, TreePanel, and ListView
24 * should subclass AbstractStoreSelectionModel and provide a way
25 * to binding to the component.
27 * The abstract methods onSelectChange and onLastFocusChanged should
28 * be implemented in these subclasses to update the UI widget.
30 Ext.define('Ext.selection.Model', {
31 extend: 'Ext.util.Observable',
32 alternateClassName: 'Ext.AbstractSelectionModel',
33 requires: ['Ext.data.StoreManager'],
39 * Valid values are SINGLE, SIMPLE, and MULTI. Defaults to 'SINGLE'
43 * @cfg {Boolean} allowDeselect
44 * Allow users to deselect a record in a DataView, List or Grid. Only applicable when the SelectionModel's mode is 'SINGLE'. Defaults to false.
50 * READ-ONLY A MixedCollection that maintains all of the currently selected
57 * Prune records when they are removed from the store from the selection.
58 * This is a private flag. For an example of its usage, take a look at
59 * Ext.selection.TreeModel.
64 constructor: function(cfg) {
72 * @event selectionchange
73 * Fired after a selection change has occurred
74 * @param {Ext.selection.Model} this
75 * @param {Array} selected The selected records
86 // sets this.selectionMode
87 me.setSelectionMode(cfg.mode || me.mode);
89 // maintains the currently selected records.
90 me.selected = Ext.create('Ext.util.MixedCollection');
92 me.callParent(arguments);
95 // binds the store to the selModel.
96 bind : function(store, initial){
99 if(!initial && me.store){
100 if(store !== me.store && me.store.autoDestroy){
103 me.store.un("add", me.onStoreAdd, me);
104 me.store.un("clear", me.onStoreClear, me);
105 me.store.un("remove", me.onStoreRemove, me);
106 me.store.un("update", me.onStoreUpdate, me);
110 store = Ext.data.StoreManager.lookup(store);
113 clear: me.onStoreClear,
114 remove: me.onStoreRemove,
115 update: me.onStoreUpdate,
120 if(store && !initial) {
126 * Select all records in the view.
127 * @param {Boolean} suppressEvent True to suppress any selects event
129 selectAll: function(suppressEvent) {
131 selections = me.store.getRange(),
133 len = selections.length,
134 start = me.getSelection().length;
136 me.bulkChange = true;
137 for (; i < len; i++) {
138 me.doSelect(selections[i], true, suppressEvent);
140 delete me.bulkChange;
141 // fire selection change only if the number of selections differs
142 me.maybeFireSelectionChange(me.getSelection().length !== start);
146 * Deselect all records in the view.
147 * @param {Boolean} suppressEvent True to suppress any deselect events
149 deselectAll: function(suppressEvent) {
151 selections = me.getSelection(),
153 len = selections.length,
154 start = me.getSelection().length;
156 me.bulkChange = true;
157 for (; i < len; i++) {
158 me.doDeselect(selections[i], suppressEvent);
160 delete me.bulkChange;
161 // fire selection change only if the number of selections differs
162 me.maybeFireSelectionChange(me.getSelection().length !== start);
165 // Provides differentiation of logic between MULTI, SIMPLE and SINGLE
166 // selection modes. Requires that an event be passed so that we can know
167 // if user held ctrl or shift.
168 selectWithEvent: function(record, e, keepExisting) {
171 switch (me.selectionMode) {
173 if (e.ctrlKey && me.isSelected(record)) {
174 me.doDeselect(record, false);
175 } else if (e.shiftKey && me.lastFocused) {
176 me.selectRange(me.lastFocused, record, e.ctrlKey);
177 } else if (e.ctrlKey) {
178 me.doSelect(record, true, false);
179 } else if (me.isSelected(record) && !e.shiftKey && !e.ctrlKey && me.selected.getCount() > 1) {
180 me.doSelect(record, keepExisting, false);
182 me.doSelect(record, false);
186 if (me.isSelected(record)) {
187 me.doDeselect(record);
189 me.doSelect(record, true);
193 // if allowDeselect is on and this record isSelected, deselect it
194 if (me.allowDeselect && me.isSelected(record)) {
195 me.doDeselect(record);
196 // select the record and do NOT maintain existing selections
198 me.doSelect(record, false);
205 * Selects a range of rows if the selection model {@link #isLocked is not locked}.
206 * All rows in between startRow and endRow are also selected.
207 * @param {Ext.data.Model/Number} startRow The record or index of the first row in the range
208 * @param {Ext.data.Model/Number} endRow The record or index of the last row in the range
209 * @param {Boolean} keepExisting (optional) True to retain existing selections
211 selectRange : function(startRow, endRow, keepExisting, dir){
225 me.deselectAll(true);
228 if (!Ext.isNumber(startRow)) {
229 startRow = store.indexOf(startRow);
231 if (!Ext.isNumber(endRow)) {
232 endRow = store.indexOf(endRow);
236 if (startRow > endRow){
242 for (i = startRow; i <= endRow; i++) {
243 if (me.isSelected(store.getAt(i))) {
251 dontDeselect = (dir == 'up') ? startRow : endRow;
254 for (i = startRow; i <= endRow; i++){
255 if (selectedCount == (endRow - startRow + 1)) {
256 if (i != dontDeselect) {
257 me.doDeselect(i, true);
260 records.push(store.getAt(i));
263 me.doMultiSelect(records, true);
267 * Selects a record instance by record instance or index.
268 * @param {Ext.data.Model/Index} records An array of records or an index
269 * @param {Boolean} keepExisting
270 * @param {Boolean} suppressEvent Set to false to not fire a select event
272 select: function(records, keepExisting, suppressEvent) {
273 this.doSelect(records, keepExisting, suppressEvent);
277 * Deselects a record instance by record instance or index.
278 * @param {Ext.data.Model/Index} records An array of records or an index
279 * @param {Boolean} suppressEvent Set to false to not fire a deselect event
281 deselect: function(records, suppressEvent) {
282 this.doDeselect(records, suppressEvent);
285 doSelect: function(records, keepExisting, suppressEvent) {
292 if (typeof records === "number") {
293 records = [me.store.getAt(records)];
295 if (me.selectionMode == "SINGLE" && records) {
296 record = records.length ? records[0] : records;
297 me.doSingleSelect(record, suppressEvent);
299 me.doMultiSelect(records, keepExisting, suppressEvent);
303 doMultiSelect: function(records, keepExisting, suppressEvent) {
305 selected = me.selected,
315 records = !Ext.isArray(records) ? [records] : records;
316 len = records.length;
317 if (!keepExisting && selected.getCount() > 0) {
318 if (me.doDeselect(me.getSelection(), suppressEvent) === false) {
321 // TODO - coalesce the selectionchange event in deselect w/the one below...
325 selected.add(record);
329 for (; i < len; i++) {
331 if (keepExisting && me.isSelected(record)) {
334 me.lastSelected = record;
336 me.onSelectChange(record, true, suppressEvent, commit);
338 me.setLastFocused(record, suppressEvent);
339 // fire selchange if there was a change and there is no suppressEvent flag
340 me.maybeFireSelectionChange(change && !suppressEvent);
343 // records can be an index, a record or an array of records
344 doDeselect: function(records, suppressEvent) {
346 selected = me.selected,
356 if (typeof records === "number") {
357 records = [me.store.getAt(records)];
358 } else if (!Ext.isArray(records)) {
364 selected.remove(record);
367 len = records.length;
369 for (; i < len; i++) {
371 if (me.isSelected(record)) {
372 if (me.lastSelected == record) {
373 me.lastSelected = selected.last();
376 me.onSelectChange(record, false, suppressEvent, commit);
380 // fire selchange if there was a change and there is no suppressEvent flag
381 me.maybeFireSelectionChange(accepted > 0 && !suppressEvent);
382 return accepted === attempted;
385 doSingleSelect: function(record, suppressEvent) {
388 selected = me.selected;
394 // should we also check beforeselect?
395 if (me.isSelected(record)) {
400 me.bulkChange = true;
401 if (selected.getCount() > 0 && me.doDeselect(me.lastSelected, suppressEvent) === false) {
402 delete me.bulkChange;
405 delete me.bulkChange;
407 selected.add(record);
408 me.lastSelected = record;
412 me.onSelectChange(record, true, suppressEvent, commit);
415 if (!suppressEvent) {
416 me.setLastFocused(record);
418 me.maybeFireSelectionChange(!suppressEvent);
423 * @param {Ext.data.Model} record
424 * Set a record as the last focused record. This does NOT mean
425 * that the record has been selected.
427 setLastFocused: function(record, supressFocus) {
429 recordBeforeLast = me.lastFocused;
430 me.lastFocused = record;
431 me.onLastFocusChanged(recordBeforeLast, record, supressFocus);
435 * Determines if this record is currently focused.
436 * @param Ext.data.Record record
438 isFocused: function(record) {
439 return record === this.getLastFocused();
443 // fire selection change as long as true is not passed
444 // into maybeFireSelectionChange
445 maybeFireSelectionChange: function(fireEvent) {
447 if (fireEvent && !me.bulkChange) {
448 me.fireEvent('selectionchange', me, me.getSelection());
453 * Returns the last selected record.
455 getLastSelected: function() {
456 return this.lastSelected;
459 getLastFocused: function() {
460 return this.lastFocused;
464 * Returns an array of the currently selected records.
465 * @return {Array} The selected records
467 getSelection: function() {
468 return this.selected.getRange();
472 * Returns the current selectionMode. SINGLE, MULTI or SIMPLE.
473 * @return {String} The selectionMode
475 getSelectionMode: function() {
476 return this.selectionMode;
480 * Sets the current selectionMode. SINGLE, MULTI or SIMPLE.
482 setSelectionMode: function(selMode) {
483 selMode = selMode ? selMode.toUpperCase() : 'SINGLE';
484 // set to mode specified unless it doesnt exist, in that case
486 this.selectionMode = this.modes[selMode] ? selMode : 'SINGLE';
490 * Returns true if the selections are locked.
493 isLocked: function() {
498 * Locks the current selection and disables any changes from
499 * happening to the selection.
500 * @param {Boolean} locked
502 setLocked: function(locked) {
503 this.locked = !!locked;
507 * Returns <tt>true</tt> if the specified row is selected.
508 * @param {Record/Number} record The record or index of the record to check
511 isSelected: function(record) {
512 record = Ext.isNumber(record) ? this.store.getAt(record) : record;
513 return this.selected.indexOf(record) !== -1;
517 * Returns true if there are any a selected records.
520 hasSelection: function() {
521 return this.selected.getCount() > 0;
524 refresh: function() {
527 oldSelections = me.getSelection(),
528 len = oldSelections.length,
532 lastFocused = this.getLastFocused();
534 // check to make sure that there are no records
535 // missing after the refresh was triggered, prune
536 // them from what is to be selected if so
537 for (; i < len; i++) {
538 selection = oldSelections[i];
539 if (!this.pruneRemoved || me.store.indexOf(selection) !== -1) {
540 toBeSelected.push(selection);
544 // there was a change from the old selected and
546 if (me.selected.getCount() != toBeSelected.length) {
550 me.clearSelections();
552 if (me.store.indexOf(lastFocused) !== -1) {
553 // restore the last focus but supress restoring focus
554 this.setLastFocused(lastFocused, true);
557 if (toBeSelected.length) {
558 // perform the selection again
559 me.doSelect(toBeSelected, false, true);
562 me.maybeFireSelectionChange(change);
566 * A fast reset of the selections without firing events, updating the ui, etc.
567 * For private usage only.
570 clearSelections: function() {
571 // reset the entire selection to nothing
572 this.selected.clear();
573 this.lastSelected = null;
574 this.setLastFocused(null);
577 // when a record is added to a store
578 onStoreAdd: function() {
582 // when a store is cleared remove all selections
583 // (if there were any)
584 onStoreClear: function() {
585 if (this.selected.getCount > 0) {
586 this.clearSelections();
587 this.maybeFireSelectionChange(true);
591 // prune records from the SelectionModel if
592 // they were selected at the time they were
594 onStoreRemove: function(store, record) {
596 selected = me.selected;
598 if (me.locked || !me.pruneRemoved) {
602 if (selected.remove(record)) {
603 if (me.lastSelected == record) {
604 me.lastSelected = null;
606 if (me.getLastFocused() == record) {
607 me.setLastFocused(null);
609 me.maybeFireSelectionChange(true);
614 * Gets the count of selected records.
615 * @return {Number} The number of selected records
617 getCount: function() {
618 return this.selected.getCount();
622 destroy: function() {
626 // if records are updated
627 onStoreUpdate: function() {
632 onSelectChange: function(record, isSelected, suppressEvent) {
637 onLastFocusChanged: function(oldFocused, newFocused) {
642 onEditorKey: function(field, e) {
647 bindComponent: function(cmp) {