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 * Tracks what records are currently selected in a databound component.
18 * This is an abstract class and is not meant to be directly used. Databound UI widgets such as
19 * {@link Ext.grid.Panel Grid} and {@link Ext.tree.Panel Tree} should subclass Ext.selection.Model
20 * and provide a way to binding to the component.
22 * The abstract methods `onSelectChange` and `onLastFocusChanged` should be implemented in these
23 * subclasses to update the UI widget.
25 Ext.define('Ext.selection.Model', {
26 extend: 'Ext.util.Observable',
27 alternateClassName: 'Ext.AbstractSelectionModel',
28 requires: ['Ext.data.StoreManager'],
33 * Mode of selection. Valid values are:
35 * - **SINGLE** - Only allows selecting one item at a time. Use {@link #allowDeselect} to allow
36 * deselecting that item. This is the default.
37 * - **SIMPLE** - Allows simple selection of multiple items one-by-one. Each click in grid will either
38 * select or deselect an item.
39 * - **MULTI** - Allows complex selection of multiple items using Ctrl and Shift keys.
43 * @cfg {Boolean} allowDeselect
44 * Allow users to deselect a record in a DataView, List or Grid.
45 * Only applicable when the {@link #mode} is 'SINGLE'.
50 * @property {Ext.util.MixedCollection} selected
51 * A MixedCollection that maintains all of the currently selected records. Read-only.
56 * Prune records when they are removed from the store from the selection.
57 * This is a private flag. For an example of its usage, take a look at
58 * Ext.selection.TreeModel.
63 constructor: function(cfg) {
72 * Fired after a selection change has occurred
73 * @param {Ext.selection.Model} this
74 * @param {Ext.data.Model[]} selected The selected records
85 // sets this.selectionMode
86 me.setSelectionMode(cfg.mode || me.mode);
88 // maintains the currently selected records.
89 me.selected = Ext.create('Ext.util.MixedCollection');
91 me.callParent(arguments);
94 // binds the store to the selModel.
95 bind : function(store, initial){
98 if(!initial && me.store){
99 if(store !== me.store && me.store.autoDestroy){
100 me.store.destroyStore();
102 me.store.un("add", me.onStoreAdd, me);
103 me.store.un("clear", me.onStoreClear, me);
104 me.store.un("remove", me.onStoreRemove, me);
105 me.store.un("update", me.onStoreUpdate, me);
109 store = Ext.data.StoreManager.lookup(store);
112 clear: me.onStoreClear,
113 remove: me.onStoreRemove,
114 update: me.onStoreUpdate,
119 if(store && !initial) {
125 * Selects all records in the view.
126 * @param {Boolean} suppressEvent True to suppress any select events
128 selectAll: function(suppressEvent) {
130 selections = me.store.getRange(),
132 len = selections.length,
133 start = me.getSelection().length;
135 me.bulkChange = true;
136 for (; i < len; i++) {
137 me.doSelect(selections[i], true, suppressEvent);
139 delete me.bulkChange;
140 // fire selection change only if the number of selections differs
141 me.maybeFireSelectionChange(me.getSelection().length !== start);
145 * Deselects all records in the view.
146 * @param {Boolean} suppressEvent True to suppress any deselect events
148 deselectAll: function(suppressEvent) {
150 selections = me.getSelection(),
152 len = selections.length,
153 start = me.getSelection().length;
155 me.bulkChange = true;
156 for (; i < len; i++) {
157 me.doDeselect(selections[i], suppressEvent);
159 delete me.bulkChange;
160 // fire selection change only if the number of selections differs
161 me.maybeFireSelectionChange(me.getSelection().length !== start);
164 // Provides differentiation of logic between MULTI, SIMPLE and SINGLE
165 // selection modes. Requires that an event be passed so that we can know
166 // if user held ctrl or shift.
167 selectWithEvent: function(record, e, keepExisting) {
170 switch (me.selectionMode) {
172 if (e.ctrlKey && me.isSelected(record)) {
173 me.doDeselect(record, false);
174 } else if (e.shiftKey && me.lastFocused) {
175 me.selectRange(me.lastFocused, record, e.ctrlKey);
176 } else if (e.ctrlKey) {
177 me.doSelect(record, true, false);
178 } else if (me.isSelected(record) && !e.shiftKey && !e.ctrlKey && me.selected.getCount() > 1) {
179 me.doSelect(record, keepExisting, false);
181 me.doSelect(record, false);
185 if (me.isSelected(record)) {
186 me.doDeselect(record);
188 me.doSelect(record, true);
192 // if allowDeselect is on and this record isSelected, deselect it
193 if (me.allowDeselect && me.isSelected(record)) {
194 me.doDeselect(record);
195 // select the record and do NOT maintain existing selections
197 me.doSelect(record, false);
204 * Selects a range of rows if the selection model {@link #isLocked is not locked}.
205 * All rows in between startRow and endRow are also selected.
206 * @param {Ext.data.Model/Number} startRow The record or index of the first row in the range
207 * @param {Ext.data.Model/Number} endRow The record or index of the last row in the range
208 * @param {Boolean} [keepExisting] True to retain existing selections
210 selectRange : function(startRow, endRow, keepExisting, dir){
224 me.deselectAll(true);
227 if (!Ext.isNumber(startRow)) {
228 startRow = store.indexOf(startRow);
230 if (!Ext.isNumber(endRow)) {
231 endRow = store.indexOf(endRow);
235 if (startRow > endRow){
241 for (i = startRow; i <= endRow; i++) {
242 if (me.isSelected(store.getAt(i))) {
250 dontDeselect = (dir == 'up') ? startRow : endRow;
253 for (i = startRow; i <= endRow; i++){
254 if (selectedCount == (endRow - startRow + 1)) {
255 if (i != dontDeselect) {
256 me.doDeselect(i, true);
259 records.push(store.getAt(i));
262 me.doMultiSelect(records, true);
266 * Selects a record instance by record instance or index.
267 * @param {Ext.data.Model[]/Number} records An array of records or an index
268 * @param {Boolean} [keepExisting] True to retain existing selections
269 * @param {Boolean} [suppressEvent] Set to true to not fire a select event
271 select: function(records, keepExisting, suppressEvent) {
272 // Automatically selecting eg store.first() or store.last() will pass undefined, so that must just return;
273 if (Ext.isDefined(records)) {
274 this.doSelect(records, keepExisting, suppressEvent);
279 * Deselects a record instance by record instance or index.
280 * @param {Ext.data.Model[]/Number} records An array of records or an index
281 * @param {Boolean} [suppressEvent] Set to true to not fire a deselect event
283 deselect: function(records, suppressEvent) {
284 this.doDeselect(records, suppressEvent);
287 doSelect: function(records, keepExisting, suppressEvent) {
294 if (typeof records === "number") {
295 records = [me.store.getAt(records)];
297 if (me.selectionMode == "SINGLE" && records) {
298 record = records.length ? records[0] : records;
299 me.doSingleSelect(record, suppressEvent);
301 me.doMultiSelect(records, keepExisting, suppressEvent);
305 doMultiSelect: function(records, keepExisting, suppressEvent) {
307 selected = me.selected,
317 records = !Ext.isArray(records) ? [records] : records;
318 len = records.length;
319 if (!keepExisting && selected.getCount() > 0) {
320 if (me.doDeselect(me.getSelection(), suppressEvent) === false) {
323 // TODO - coalesce the selectionchange event in deselect w/the one below...
327 selected.add(record);
331 for (; i < len; i++) {
333 if (keepExisting && me.isSelected(record)) {
336 me.lastSelected = record;
338 me.onSelectChange(record, true, suppressEvent, commit);
340 me.setLastFocused(record, suppressEvent);
341 // fire selchange if there was a change and there is no suppressEvent flag
342 me.maybeFireSelectionChange(change && !suppressEvent);
345 // records can be an index, a record or an array of records
346 doDeselect: function(records, suppressEvent) {
348 selected = me.selected,
358 if (typeof records === "number") {
359 records = [me.store.getAt(records)];
360 } else if (!Ext.isArray(records)) {
366 selected.remove(record);
369 len = records.length;
371 for (; i < len; i++) {
373 if (me.isSelected(record)) {
374 if (me.lastSelected == record) {
375 me.lastSelected = selected.last();
378 me.onSelectChange(record, false, suppressEvent, commit);
382 // fire selchange if there was a change and there is no suppressEvent flag
383 me.maybeFireSelectionChange(accepted > 0 && !suppressEvent);
384 return accepted === attempted;
387 doSingleSelect: function(record, suppressEvent) {
390 selected = me.selected;
396 // should we also check beforeselect?
397 if (me.isSelected(record)) {
402 me.bulkChange = true;
403 if (selected.getCount() > 0 && me.doDeselect(me.lastSelected, suppressEvent) === false) {
404 delete me.bulkChange;
407 delete me.bulkChange;
409 selected.add(record);
410 me.lastSelected = record;
414 me.onSelectChange(record, true, suppressEvent, commit);
417 if (!suppressEvent) {
418 me.setLastFocused(record);
420 me.maybeFireSelectionChange(!suppressEvent);
425 * Sets a record as the last focused record. This does NOT mean
426 * that the record has been selected.
427 * @param {Ext.data.Model} record
429 setLastFocused: function(record, supressFocus) {
431 recordBeforeLast = me.lastFocused;
432 me.lastFocused = record;
433 me.onLastFocusChanged(recordBeforeLast, record, supressFocus);
437 * Determines if this record is currently focused.
438 * @param {Ext.data.Model} record
440 isFocused: function(record) {
441 return record === this.getLastFocused();
445 // fire selection change as long as true is not passed
446 // into maybeFireSelectionChange
447 maybeFireSelectionChange: function(fireEvent) {
449 if (fireEvent && !me.bulkChange) {
450 me.fireEvent('selectionchange', me, me.getSelection());
455 * Returns the last selected record.
457 getLastSelected: function() {
458 return this.lastSelected;
461 getLastFocused: function() {
462 return this.lastFocused;
466 * Returns an array of the currently selected records.
467 * @return {Ext.data.Model[]} The selected records
469 getSelection: function() {
470 return this.selected.getRange();
474 * Returns the current selectionMode.
475 * @return {String} The selectionMode: 'SINGLE', 'MULTI' or 'SIMPLE'.
477 getSelectionMode: function() {
478 return this.selectionMode;
482 * Sets the current selectionMode.
483 * @param {String} selModel 'SINGLE', 'MULTI' or 'SIMPLE'.
485 setSelectionMode: function(selMode) {
486 selMode = selMode ? selMode.toUpperCase() : 'SINGLE';
487 // set to mode specified unless it doesnt exist, in that case
489 this.selectionMode = this.modes[selMode] ? selMode : 'SINGLE';
493 * Returns true if the selections are locked.
496 isLocked: function() {
501 * Locks the current selection and disables any changes from happening to the selection.
502 * @param {Boolean} locked True to lock, false to unlock.
504 setLocked: function(locked) {
505 this.locked = !!locked;
509 * Returns true if the specified row is selected.
510 * @param {Ext.data.Model/Number} record The record or index of the record to check
513 isSelected: function(record) {
514 record = Ext.isNumber(record) ? this.store.getAt(record) : record;
515 return this.selected.indexOf(record) !== -1;
519 * Returns true if there are any a selected records.
522 hasSelection: function() {
523 return this.selected.getCount() > 0;
526 refresh: function() {
529 oldSelections = me.getSelection(),
530 len = oldSelections.length,
534 lastFocused = this.getLastFocused();
536 // check to make sure that there are no records
537 // missing after the refresh was triggered, prune
538 // them from what is to be selected if so
539 for (; i < len; i++) {
540 selection = oldSelections[i];
541 if (!this.pruneRemoved || me.store.indexOf(selection) !== -1) {
542 toBeSelected.push(selection);
546 // there was a change from the old selected and
548 if (me.selected.getCount() != toBeSelected.length) {
552 me.clearSelections();
554 if (me.store.indexOf(lastFocused) !== -1) {
555 // restore the last focus but supress restoring focus
556 this.setLastFocused(lastFocused, true);
559 if (toBeSelected.length) {
560 // perform the selection again
561 me.doSelect(toBeSelected, false, true);
564 me.maybeFireSelectionChange(change);
568 * A fast reset of the selections without firing events, updating the ui, etc.
569 * For private usage only.
572 clearSelections: function() {
573 // reset the entire selection to nothing
574 this.selected.clear();
575 this.lastSelected = null;
576 this.setLastFocused(null);
579 // when a record is added to a store
580 onStoreAdd: function() {
584 // when a store is cleared remove all selections
585 // (if there were any)
586 onStoreClear: function() {
587 if (this.selected.getCount > 0) {
588 this.clearSelections();
589 this.maybeFireSelectionChange(true);
593 // prune records from the SelectionModel if
594 // they were selected at the time they were
596 onStoreRemove: function(store, record) {
598 selected = me.selected;
600 if (me.locked || !me.pruneRemoved) {
604 if (selected.remove(record)) {
605 if (me.lastSelected == record) {
606 me.lastSelected = null;
608 if (me.getLastFocused() == record) {
609 me.setLastFocused(null);
611 me.maybeFireSelectionChange(true);
616 * Returns the count of selected records.
617 * @return {Number} The number of selected records
619 getCount: function() {
620 return this.selected.getCount();
624 destroy: function() {
628 // if records are updated
629 onStoreUpdate: function() {
634 onSelectChange: function(record, isSelected, suppressEvent) {
639 onLastFocusChanged: function(oldFocused, newFocused) {
644 onEditorKey: function(field, e) {
649 bindComponent: function(cmp) {