/** * Tracks what records are currently selected in a data-bound component. * * This is an abstract class and is not meant to be directly used. Data-bound UI widgets such as * {@link Ext.grid.Panel Grid} and {@link Ext.tree.Panel Tree} should subclass Ext.selection.Model * and provide a way to binding to the component. * * The abstract method `onSelectChange` should be implemented in these * subclasses to update the UI widget. */Ext.define('Ext.selection.Model', { extend: 'Ext.mixin.Observable', alternateClassName: 'Ext.AbstractSelectionModel', alias: 'selection.abstract', requires: [ 'Ext.util.Bag' ], mixins: [ 'Ext.util.StoreHolder', 'Ext.mixin.Factoryable' ], factoryConfig: { // Need to override the defaultType, otherwise this class would be the default, and it is an abstract base. defaultType: 'dataviewmodel' }, // We do not want "_hidden" style backing properties. $configPrefixed: false, // We also want non-config system properties to go to the instance. $configStrict: false, config: { /** * @private * The {@link Ext.data.Store store} in which this selection model represents the selected subset. */ store: null, /** * @private * The {@link Ext.util.Bag/Ext.util.Collection} to use as the collection of selected records. */ selected: {} }, // lastSelected /** * @property {Boolean} isSelectionModel * `true` in this class to identify an object as an instantiated {@link Ext.selection.Model selection model}, or subclass thereof. */ isSelectionModel: true, /** * @cfg {"SINGLE"/"SIMPLE"/"MULTI"} mode * Mode of selection. Valid values are: * * - **"SINGLE"** - Only allows selecting one item at a time. Use {@link #allowDeselect} to allow * deselecting that item. Also see {@link #toggleOnClick}. This is the default. * - **"SIMPLE"** - Allows simple selection of multiple items one-by-one. Each click in grid will either * select or deselect an item. * - **"MULTI"** - Allows complex selection of multiple items using Ctrl and Shift keys. */ /** * @cfg {Boolean} allowDeselect * Allow users to deselect a record in a DataView, List or Grid. * Only applicable when the {@link #mode} is 'SINGLE'. */ allowDeselect: undefined, /** * @cfg {Boolean} toggleOnClick * `true` to toggle the selection state of an item when clicked. * Only applicable when the {@link #mode} is 'SINGLE'. * Only applicable when the {@link #allowDeselect} is 'true'. */ toggleOnClick: true, /** * @cfg {Boolean} ordered * If this is set to `true`, the selected items will be tracked in an instance of * {Ext.util.Collection} instead of {Ext.util.Bag} to maintain insertion order. * @private */ ordered: false, /** * @property {Ext.util.Bag/Ext.util.Collection} selected * A collection that maintains all of the currently selected records. The type * of collection class depends on the `ordered` config. * @readonly * @private */ selected: null, /** * @cfg {Boolean} [pruneRemoved=true] * Remove records from the selection when they are removed from the store. * * **Important:** When using {@link Ext.toolbar.Paging paging} or a {@link Ext.data.BufferedStore}, * records which are cached in the Store's {@link Ext.data.Store#property-data data collection} may be removed from the Store when pages change, * or when rows are scrolled out of view. For this reason `pruneRemoved` should be set to `false` when using a buffered Store. * * Also, when previously pruned pages are returned to the cache, the records objects in the page will be * *new instances*, and will not match the instances in the selection model's collection. For this reason, * you MUST ensure that the Model definition's {@link Ext.data.Model#idProperty idProperty} references a unique * key because in this situation, records in the Store have their **IDs** compared to records in the SelectionModel * in order to re-select a record which is scrolled back into view. */ pruneRemoved: true, suspendChange: 0, /** * @cfg {Boolean} [ignoreRightMouseSelection=false] * True to ignore selections that are made when using the right mouse button if there are * records that are already selected. If no records are selected, selection will continue * as normal */ ignoreRightMouseSelection: false, /** * @event selectionchange * Fired after a selection change has occurred * @param {Ext.selection.Model} this * @param {Ext.data.Model[]} selected The selected records */ /** * @event focuschange * Fired when a row is focused * @param {Ext.selection.Model} this * @param {Ext.data.Model} oldFocused The previously focused record * @param {Ext.data.Model} newFocused The newly focused record */ constructor: function(cfg) { var me = this; me.modes = { SINGLE: true, SIMPLE: true, MULTI: true }; me.callParent([cfg]); // sets this.selectionMode me.setSelectionMode(me.mode); if (me.selectionMode !== 'SINGLE') { me.allowDeselect = true; } }, updateStore: function(store, oldStore) { this.bindStore(store, !oldStore); }, applySelected: function(selected) { if (!selected.isBag && !selected.isCollection) { selected = new Ext.util[this.ordered ? 'Collection' : 'Bag'](Ext.apply({ rootProperty: 'data' }, selected)); } return selected; }, getStoreListeners: function() { var me = this; return { add: me.onStoreAdd, clear: me.onStoreClear, remove: me.onStoreRemove, update: me.onStoreUpdate, idchanged: me.onIdChanged, load: me.onStoreLoad, refresh: me.onStoreRefresh, // BufferedStore events pageadd: me.onPageAdd, pageremove: me.onPageRemove }; }, onBindStore: function(store, oldStore, initial) { if (!initial) { this.updateSelectedInstances(this.selected); } }, suspendChanges: function() { ++this.suspendChange; }, resumeChanges: function() { if (this.suspendChange) { --this.suspendChange; } }, /** * Selects all records in the view. * @param {Boolean} suppressEvent True to suppress any select events */ selectAll: function(suppressEvent) { var me = this, selections = me.store.getRange(), start = me.getSelection().length; me.suspendChanges(); me.doSelect(selections, true, suppressEvent); if (!me.destroyed) { me.resumeChanges(); // fire selection change only if the number of selections differs if (!suppressEvent) { me.maybeFireSelectionChange(me.getSelection().length !== start); } } }, /** * Deselects all records in the view. * @param {Boolean} [suppressEvent] True to suppress any deselect events */ deselectAll: function(suppressEvent) { var me = this, selections = me.getSelection(), selIndexes = {}, store = me.store, start = selections.length, i, l, rec; // Cache selection records' indexes first to avoid // looking them up on every sort comparison below. // We can't rely on store.indexOf being fast because // for whatever reason the Store in question may force // sequential index lookup, which will result in O(n^2) // sort performance below. for (i = 0, l = selections.length; i < l; i++) { rec = selections[i]; selIndexes[rec.id] = store.indexOf(rec); } // Sort the selections so that the events fire in // a predictable order like selectAll selections = Ext.Array.sort(selections, function(r1, r2){ var idx1 = selIndexes[r1.id], idx2 = selIndexes[r2.id]; // Don't check for equality since indexes will be unique return idx1 < idx2 ? -1 : 1; }); me.suspendChanges(); me.doDeselect(selections, suppressEvent); if (!me.destroyed) { me.resumeChanges(); // fire selection change only if the number of selections differs if (!suppressEvent) { me.maybeFireSelectionChange(me.getSelection().length !== start); } } }, getSelectionStart: function () { return this.selectionStart; }, setSelectionStart: function (selection) { this.selectionStart = selection; }, // Provides differentiation of logic between MULTI, SIMPLE and SINGLE // selection modes. Requires that an event be passed so that we can know // if user held ctrl or shift. selectWithEvent: function(record, e) { var me = this, isSelected = me.isSelected(record), shift = e.shiftKey; switch (me.selectionMode) { case 'MULTI': me.selectWithEventMulti(record, e, isSelected); break; case 'SIMPLE': me.selectWithEventSimple(record, e, isSelected); break; case 'SINGLE': me.selectWithEventSingle(record, e, isSelected); break; } // Event handlers could have destroyed the selection model if (me.destroyed) { return; } // selectionStart is a start point for shift/mousedown to create a range from. // If the mousedowned record was not already selected, then it becomes the // start of any range created from now on. // If we drop to no records selected, then there is no range start any more. if (!shift) { if (me.isSelected(record)) { me.selectionStart = record; } else { me.selectionStart = null; } } }, /** * Checks whether a selection should proceed based on the ignoreRightMouseSelection * option. * @private * @param {Ext.event.Event} e The event * @return {Boolean} `true` if the selection should not proceed. */ vetoSelection: function(e) { // Flag can be stamped into the event at any time. // This is used by ActionColumn and CheckColumn to implement their stopSelection config if (e.stopSelection) { return true; } else if (e.type !== 'keydown' && e.button !== 0) { if (this.ignoreRightMouseSelection || this.isSelected(e.record)) { return true; } } else { return e.type === 'mousedown'; } }, // Private // Called in response to a FocusModel's navigate event when a new record has been navigated to. // Event is passed so that shift and ctrl can be handled. onNavigate: function(e) { // Enforce the ignoreRightMouseSelection setting. // Enforce presence of a record. // Enforce selection upon click, not mousedown. if (!e.record || this.vetoSelection(e.keyEvent)) { return; } this.onBeforeNavigate(e); var me = this, keyEvent = e.keyEvent, // ctrlKey may be set on the event if we want to treat it like a ctrlKey so // we don't mutate the original event object ctrlKey = keyEvent.ctrlKey || e.ctrlKey, recIdx = e.recordIndex, record = e.record, lastFocused = e.previousRecord, isSelected = me.isSelected(record), from = (me.selectionStart && me.isSelected(me.selectionStart)) ? me.selectionStart : (me.selectionStart = e.previousRecord), fromIdx = e.previousRecordIndex, key = keyEvent.getCharCode(), isSpace = key === keyEvent.SPACE, changedRec = e.record !== e.previousRecord, direction = key === keyEvent.UP || key === keyEvent.PAGE_UP || key === keyEvent.HOME || (key === keyEvent.LEFT && changedRec) ? 'up' : (key === keyEvent.DOWN || key === keyEvent.PAGE_DOWN || key === keyEvent.END || (key === keyEvent.RIGHT && changedRec) ? 'down' : null); switch (me.selectionMode) { case 'MULTI': me.setSelectionStart(e.selectionStart); if (key === keyEvent.A && ctrlKey) { // Listening to endUpdate on the Collection will be more efficient me.selected.beginUpdate(); me.selectRange(0, me.store.getCount() - 1); me.selected.endUpdate(); } else if (isSpace) { // SHIFT+SPACE, select range if (keyEvent.shiftKey) { me.selectRange(from, record, ctrlKey); } else { // SPACE pressed on a selected item: deselect. if (isSelected) { if (me.allowDeselect) { me.doDeselect(record); } } // SPACE on an unselected item: select it // keyEvent.ctrlKey means "keep existing" else { me.doSelect(record, ctrlKey); } } } // SHIFT-navigate selects intervening rows from the last selected (or last focused) item and target item else if (keyEvent.shiftKey && from) { // If we are heading back TOWARDS the start rec - deselect skipped range... if (direction === 'up' && fromIdx <= recIdx) { me.deselectRange(lastFocused, recIdx + 1); } else if (direction === 'down' && fromIdx >= recIdx) { me.deselectRange(lastFocused, recIdx - 1); } // If we are heading AWAY from start point, or no CTRL key, so just select the range and let the CTRL control "keepExisting"... else if (from !== record) { me.selectRange(from, record, ctrlKey); } me.lastSelected = record; } else if (key) { if (!ctrlKey) { me.doSelect(record, false); } } else { me.selectWithEvent(record, keyEvent); } break; case 'SIMPLE': if (key === keyEvent.A && ctrlKey) { // Listening to endUpdate on the Collection will be more efficient me.selected.beginUpdate(); me.selectRange(0, me.store.getCount() - 1); me.selected.endUpdate(); } else if (isSelected) { me.doDeselect(record); } else { me.doSelect(record, true); } break; case 'SINGLE': // CTRL-navigation does not select if (!ctrlKey) { // Arrow movement if (direction) { me.doSelect(record, false); } // Space or click else if (isSpace || !key) { me.selectWithEvent(record, keyEvent); } } } // selectionStart is a start point for shift/mousedown to create a range from. // If the mousedowned record was not already selected, then it becomes the // start of any range created from now on. // If we drop to no records selected, then there is no range start any more. if (!keyEvent.shiftKey && !me.destroyed && me.isSelected(record)) { me.selectionStart = record; me.selectionStartIdx = recIdx; } }, /** * Selects a range of rows if the selection model {@link #isLocked is not locked}. * All rows in between startRow and endRow are also selected. * @param {Ext.data.Model/Number} startRow The record or index of the first row in the range * @param {Ext.data.Model/Number} endRow The record or index of the last row in the range * @param {Boolean} keepExisting (optional) True to retain existing selections */ selectRange: function(startRow, endRow, keepExisting) { var me = this, store = me.store, selected = me.selected.items, result, i, len, toSelect, toDeselect, idx, rec; if (me.isLocked()){ return; } result = me.normalizeRowRange(startRow, endRow); startRow = result[0]; endRow = result[1]; toSelect = []; for (i = startRow; i <= endRow; i++){ if (!me.isSelected(store.getAt(i))) { toSelect.push(store.getAt(i)); } } if (!keepExisting) { // prevent selectionchange from firing toDeselect = []; me.suspendChanges(); for (i = 0, len = selected.length; i < len; ++i) { rec = selected[i]; idx = store.indexOf(rec); if (idx < startRow || idx > endRow) { toDeselect.push(rec); } } for (i = 0, len = toDeselect.length; i < len; ++i) { me.doDeselect(toDeselect[i]); // We could have brought destruction upon ourselves via handlers; // no point in continuing if that happened if (me.destroyed) { break; } } if (!me.destroyed) { me.resumeChanges(); } } if (!me.destroyed) { if (toSelect.length) { me.doMultiSelect(toSelect, true); } else if (toDeselect) { me.maybeFireSelectionChange(toDeselect.length > 0); } } }, /** * Deselects a range of rows if the selection model {@link #isLocked is not locked}. * @param {Ext.data.Model/Number} startRow The record or index of the first row in the range * @param {Ext.data.Model/Number} endRow The record or index of the last row in the range */ deselectRange : function(startRow, endRow) { var me = this, store = me.store, result, i, toDeselect, record; if (me.isLocked()){ return; } result = me.normalizeRowRange(startRow, endRow); startRow = result[0]; endRow = result[1]; toDeselect = []; for (i = startRow; i <= endRow; i++) { record = store.getAt(i); if (me.isSelected(record)) { toDeselect.push(record); } } if (toDeselect.length) { me.doDeselect(toDeselect); } }, normalizeRowRange: function(startRow, endRow) { var store = this.store, tmp; if (!Ext.isNumber(startRow)) { startRow = store.indexOf(startRow); } startRow = Math.max(0, startRow); if (!Ext.isNumber(endRow)) { endRow = store.indexOf(endRow); } endRow = Math.min(endRow, store.getCount() - 1); // swap values if (startRow > endRow){ tmp = endRow; endRow = startRow; startRow = tmp; } return [startRow, endRow]; }, /** * Selects a record instance by record instance or index. * @param {Ext.data.Model[]/Number} records An array of records or an index * @param {Boolean} [keepExisting=false] True to retain existing selections * @param {Boolean} [suppressEvent=false] True to not fire a select event */ select: function(records, keepExisting, suppressEvent) { // Automatically selecting eg store.first() or store.last() will pass undefined, so that must just return; if (Ext.isDefined(records) && !(Ext.isArray(records) && !records.length)) { this.doSelect(records, keepExisting, suppressEvent); } }, /** * Deselects a record instance by record instance or index. * @param {Ext.data.Model[]/Number} records An array of records or an index * @param {Boolean} [suppressEvent=false] True to not fire a deselect event */ deselect: function(records, suppressEvent) { this.doDeselect(records, suppressEvent); }, doSelect: function(records, keepExisting, suppressEvent) { var me = this, record; if (me.locked || records == null) { return; } if (typeof records === "number") { record = me.store.getAt(records); // No matching record, jump out. if (!record) { return; } records = [record]; } if (me.selectionMode === "SINGLE") { if (records.isModel) { records = [records]; } if (records.length) { me.doSingleSelect(records[0], suppressEvent); } } else { me.doMultiSelect(records, keepExisting, suppressEvent); } }, doMultiSelect: function(records, keepExisting, suppressEvent) { var me = this, selected = me.selected, change = false, result, i, len, record, commit; if (me.locked) { return; } records = !Ext.isArray(records) ? [records] : records; len = records.length; if (!keepExisting && selected.getCount() > 0) { result = me.deselectDuringSelect(records, suppressEvent); if (me.destroyed) { return; } if (result[0]) { // We had a failure during selection, so jump out // Fire selection change if we did deselect anything me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent); return; } else { // Means something has been deselected, so we've had a change change = result[1] > 0; } } commit = function() { if (!selected.getCount()) { me.selectionStart = record; } if (!suppressEvent) { selected.add(record); } change = true; }; for (i = 0; i < len; i++) { record = records[i]; if (me.isSelected(record)) { continue; } me.onSelectChange(record, true, suppressEvent, commit); if (me.destroyed) { return; } } me.lastSelected = record; if (suppressEvent) { selected.add(records); } // fire selchange if there was a change and there is no suppressEvent flag me.maybeFireSelectionChange(change && !suppressEvent); }, deselectDuringSelect: function(toSelect, suppressEvent) { var me = this, selected = me.selected.getRange(), len = selected.length, changed = 0, failed = false, item, i; // Prevent selection change events from firing, will happen during select me.suspendChanges(); me.deselectingDuringSelect = true; for (i = 0; i < len; ++i) { item = selected[i]; if (!Ext.Array.contains(toSelect, item)) { if (me.doDeselect(item, suppressEvent)) { ++changed; } else { failed = true; } } if (me.destroyed) { failed = true; changed = 0; break; } } me.deselectingDuringSelect = false; if (!me.destroyed) { me.resumeChanges(); } return [failed, changed]; }, // records can be an index, a record or an array of records doDeselect: function(records, suppressEvent) { var me = this, selected = me.selected, i = 0, len, record, attempted = 0, accepted = 0, commit; if (me.locked || !me.store) { return false; } if (typeof records === "number") { record = me.store.getAt(records); // No matching record, jump out if (!record) { return false; } records = [record]; } else if (!Ext.isArray(records)) { records = [records]; } commit = function() { ++accepted; if (!suppressEvent) { selected.remove(record); } if (record === me.selectionStart) { me.selectionStart = null; } }; len = records.length; me.suspendChanges(); for (; i < len; i++) { record = records[i]; if (me.isSelected(record)) { if (me.lastSelected === record) { me.lastSelected = selected.last(); } ++attempted; me.onSelectChange(record, false, suppressEvent, commit); if (me.destroyed) { return false; } } } me.resumeChanges(); // If we have been suppressing events, we've not been removing individual records // Remove them all in one shot. if (suppressEvent) { selected.remove(records); } // fire selchange if there was a change and there is no suppressEvent flag me.maybeFireSelectionChange(accepted > 0 && !suppressEvent); return accepted === attempted; }, doSingleSelect: function(record, suppressEvent) { var me = this, changed = false, selected = me.selected, commit; if (me.locked) { return; } // already selected. // should we also check beforeselect? if (me.isSelected(record)) { return; } commit = function() { // Deselect previous selection. if (selected.getCount()) { me.suspendChanges(); var result = me.deselectDuringSelect([record], suppressEvent); if (me.destroyed) { return; } me.resumeChanges(); if (result[0]) { // Means deselection failed, so abort return false; } } me.lastSelected = record; if (!selected.getCount()) { me.selectionStart = record; } selected.add(record); changed = true; }; me.onSelectChange(record, true, suppressEvent, commit); if (changed && !me.destroyed) { me.maybeFireSelectionChange(!suppressEvent); } }, // fire selection change as long as true is not passed // into maybeFireSelectionChange maybeFireSelectionChange: function(fireEvent) { var me = this; if (fireEvent && !me.suspendChange) { me.fireEvent('selectionchange', me, me.getSelection()); } }, /** * Returns an array of the currently selected records. * @return {Ext.data.Model[]} The selected records */ getSelection: function() { return this.selected.getRange(); }, /** * Returns the current selectionMode. * @return {String} The selectionMode: 'SINGLE', 'MULTI' or 'SIMPLE'. */ getSelectionMode: function() { return this.selectionMode; }, /** * Sets the current selectionMode. * @param {String} selMode 'SINGLE', 'MULTI' or 'SIMPLE'. */ setSelectionMode: function(selMode) { selMode = selMode ? selMode.toUpperCase() : 'SINGLE'; // set to mode specified unless it doesnt exist, in that case // use single. this.selectionMode = this.modes[selMode] ? selMode : 'SINGLE'; }, /** * Returns true if the selections are locked. * @return {Boolean} */ isLocked: function() { return this.locked; }, /** * Locks the current selection and disables any changes from happening to the selection. * @param {Boolean} locked True to lock, false to unlock. */ setLocked: function(locked) { this.locked = !!locked; }, /** * Returns true if the specified row is selected. * @param {Ext.data.Model/Number} from The start of the range to check. * @param {Ext.data.Model/Number} to The end of the range to check. * @return {Boolean} */ isRangeSelected: function(startRow, endRow) { var me = this, store = me.store, i, result; result = me.normalizeRowRange(startRow, endRow); startRow = result[0]; endRow = result[1]; // Loop through. If any of the range is not selected, the answer is false. for (i = startRow; i <= endRow; i++) { if (!me.isSelected(store.getAt(i))) { return false; } } return true; }, /** * Returns true if the specified row is selected. * @param {Ext.data.Model/Number} record The record or index of the record to check * @return {Boolean} */ isSelected: function (record) { record = Ext.isNumber(record) ? this.store.getAt(record) : record; return this.selected ? this.selected.contains(record) : false; }, /** * Returns true if there are any a selected records. * @return {Boolean} */ hasSelection: function() { var selected = this.getSelected(); return !!(selected && selected.getCount()); }, refresh: function() { var me = this, store = me.store, toBeSelected = [], toBeReAdded = [], oldSelections = me.getSelection(), len = oldSelections.length, // Will be a Collection in this and DataView classes. // Will be an Ext.grid.selection.Rows instance for Spreadsheet (does not callParent for other modes). // API used in here, getCount() and add() are common. selected = me.getSelected(), change, d, storeData, selection, rec, i; // Not been bound yet, or we have never selected anything. if (!store || !(selected.isCollection || selected.isBag || selected.isRows) || !selected.getCount()) { return; } // We need to look beneath any filtering to see if the selected records are still owned by the store storeData = store.getData(); // Attempt to get the underlying source collection to avoid filtering if (storeData.getSource) { d = storeData.getSource(); if (d) { storeData = d; } } me.refreshing = true; // Inhibit update notifications during refresh of the selected collection. selected.beginUpdate(); me.suspendChanges(); // Add currently records to the toBeSelected list if present in the Store // If they are not present, and pruneRemoved is false, we must still retain the record for (i = 0; i < len; i++) { selection = oldSelections[i]; rec = storeData.get(selection.getId()); if (rec) { toBeSelected.push(rec); } // Selected records no longer represented in Store must be retained else if (!me.pruneRemoved) { toBeReAdded.push(selection); } // In single select mode, only one record may be selected if (me.mode === 'SINGLE' && toBeReAdded.length) { break; } } // there was a change from the old selected and // the new selection if (selected.getCount() !== (toBeSelected.length + toBeReAdded.length)) { change = true; } me.clearSelections(); if (toBeSelected.length) { // perform the selection again me.doSelect(toBeSelected, false, true); } // If some of the selections were not present in the Store, but pruneRemoved is false, we must add them back if (toBeReAdded.length) { selected.add(toBeReAdded); // No records reselected. if (!me.lastSelected) { me.lastSelected = toBeReAdded[toBeReAdded.length - 1]; } } me.resumeChanges(); // If the new data caused the selection to change, announce the update using endUpdate, // Otherwise, end the update silently. // Bindings may be attached to selection - we need to coalesce changes. if (change) { selected.endUpdate(); } else { selected.updating--; } me.refreshing = false; me.maybeFireSelectionChange(change); }, /** * A fast reset of the selections without firing events, updating the ui, etc. * For private usage only. * @private */ clearSelections: function() { // Will be a Collection in this and DataView classes. // Will be an Ext.grid.selection.Selection instance for Spreadsheet. // API used in here, clear() is common. var selected = this.getSelected(); // reset the entire selection to nothing if (selected) { selected.clear(); } this.lastSelected = null; }, // when a record is added to a store onStoreAdd: Ext.emptyFn, // when a store is cleared remove all selections // (if there were any) onStoreClear: function() { // When the store is clearing for reload and there is outstanding selection, // make sure to clear it! Otherwise we might end up with incosistent state. if ((!this.store.isLoading() || this.store.clearing) && this.hasSelection()) { this.clearSelections(); this.maybeFireSelectionChange(true); } }, // prune records from the SelectionModel if // they were selected at the time they were // removed. onStoreRemove: function(store, records, index, isMove) { var me = this, toDeselect = records, i, len, rec, moveMap; // If the selection start point is among records being removed, we no longer have a selection start point. if (me.selectionStart && Ext.Array.contains(records, me.selectionStart)) { me.selectionStart = null; } if (isMove || me.locked || !me.pruneRemoved) { return; } // Do a cheap check to see if the store is doing any moves before we branch into here moveMap = store.isMoving(null, true); if (moveMap) { toDeselect = null; for (i = 0, len = records.length; i < len; ++i) { rec = records[i]; if (!moveMap[rec.id]) { (toDeselect || (toDeselect = [])).push(rec); } } } if (toDeselect) { me.deselect(toDeselect); } }, // Page evicted from BufferedStore. // Remove any selections in that page unless pruneRemoved is false onPageRemove: function(pageMap, pageNumber, records) { this.onStoreRemove(this.store, records); }, // Page added to BufferedStore. // Check for return of already selected records onPageAdd: function(pageMap, pageNumber, records) { var len = records.length, i, record; for (i = 0; i < len; i++) { record = records[i]; if (this.selected.get(record.id)) { this.selected.replace(record); } } }, /** * Returns the count of selected records. * @return {Number} The number of selected records */ getCount: function() { return this.selected.getCount(); }, // Called when the contents of the node are updated, perform any processing here. onUpdate: Ext.emptyFn, // cleanup. destroy: function() { var me = this; me.clearSelections(); me.bindStore(null); me.selected = Ext.destroy(me.selected); me.callParent(); }, // if records are updated onStoreUpdate: Ext.emptyFn, onIdChanged: function(store, rec, oldId, newId) { this.selected.updateKey(rec, oldId); }, onStoreRefresh: function() { this.updateSelectedInstances(this.selected); }, /** * @private * Called when the store is refreshed. * Selected records which are no longer present in the store are removed if {@link #pruneRemoved} is `true`. * * Selected records which are still present have their instances in the passed collection updated. * @param {Ext.util.Collection/Ext.util.Bag} selected A Collection representing the currently selected records. */ updateSelectedInstances: function(selected) { var me = this, store = me.getStore(), lastSelected = me.lastSelected, removeCount = 0, prune = me.pruneRemovedOnRefresh(), items, length, i, selectedRec, rec, lastSelectedChanged; if (store && store.isBufferedStore) { return; } items = selected.getRange(); length = items.length; if (lastSelected) { me.lastSelected = store.getById(lastSelected.id); lastSelectedChanged = me.lastSelected !== lastSelected; } // Flag so that reactors to collectionEndUpdate know that the collection is not really changing me.refreshing = true; if (store) { for (i = 0; i < length; ++i) { selectedRec = items[i]; // Is the selected record ID still present in the store? rec = store.getById(selectedRec.id); // Yes, ensure the instance is correct if (rec) { if (rec !== selectedRec) { // Silently replace the stale record instance with the new record by the same ID selected.add(rec); } } // No, remove it from the selection if we are configured to prune removed records else if (prune) { selected.remove(selectedRec); ++removeCount; } } } else { removeCount = selected.getCount(); selected.removeAll(); } me.refreshing = false; me.maybeFireSelectionChange(removeCount > 0); if (lastSelectedChanged) { // Private event for now me.fireEvent('lastselectedchanged', me, me.getSelection(), lastSelected); } }, // onStoreRefresh asks if it should remove from the selection any selected records which are no // longer findable in the store after the refresh. // Subclasses may override this. // TreeModel does not use the pruneRemoved flag because records are being added and removed // from TreeStores on expand and collapse. It uses the pruneRemovedNodes flag. pruneRemovedOnRefresh: function() { return this.pruneRemoved; }, /** * @method * @abstract * @private */ onStoreLoad: Ext.emptyFn, /** * @abstract */ onSelectChange: function(record, isSelected, suppressEvent, commitFn) { var me = this, eventName = isSelected ? 'select' : 'deselect'; if ((suppressEvent || me.fireEvent('before' + eventName, me, record)) !== false && commitFn() !== false) { // Could be destroyed in the handler if (!suppressEvent && !me.destroyed) { me.fireEvent(eventName, me, record); } } }, /** * @method * @abstract */ onEditorKey: Ext.emptyFn, /** * @protected * @template * Allows multiple views to be controlled by one selection model. * Called by AbstractView's beforeRender method. * @param {Ext.view.View} view The View passes itself */ beforeViewRender: function(view) { Ext.Array.include(this.views || (this.views = []), view); }, /** * @method * @protected * @template * Called by the owning grid's {@link Ext.grid.header.Container header container} * when a column header is activated by the UI (clicked, or receives a `SPACE` or `ENTER` key event). */ onHeaderClick: Ext.emptyFn, resolveListenerScope: function(defaultScope) { var view = this.view, scope; if (view) { scope = view.resolveSatelliteListenerScope(this, defaultScope); } return scope || this.callParent([defaultScope]); }, /** * @method * @abstract */ bindComponent: Ext.emptyFn, privates: { onBeforeNavigate: Ext.privateFn, /** * @private * Called by {@link Ext.panel.Table#updateBindSelection} when publishing the `selection` property. * It should yield the last record selected. * @return {Ext.data.Model} The last record selected. This is only available if the current selection type is cells or rows. * In the case of multiple selection, the *last* record added to the selection is returned. */ getLastSelected: function() { return this.lastSelected; }, selectWithEventMulti: function(record, e, isSelected) { var me = this, shift = e.shiftKey, ctrl = e.ctrlKey, start = shift ? (me.getSelectionStart()) : null, selected = me.getSelection(), len = selected.length, toDeselect, i, item; if (shift && start) { me.selectRange(start, record, ctrl); } else if (ctrl && isSelected) { if (me.allowDeselect) { me.doDeselect(record, false); } } else if (ctrl) { me.doSelect(record, true, false); } else if (isSelected && !shift && !ctrl && len > 1) { if (me.allowDeselect) { toDeselect = []; for (i = 0; i < len; ++i) { item = selected[i]; if (item !== record) { toDeselect.push(item); } } me.doDeselect(toDeselect); } } else if (!isSelected) { me.doSelect(record, false); } }, selectWithEventSimple: function(record, e, isSelected) { if (isSelected) { this.doDeselect(record); } else { this.doSelect(record, true); } }, selectWithEventSingle: function(record, e, isSelected) { var me = this, allowDeselect = me.allowDeselect; if (allowDeselect && !e.ctrlKey) { allowDeselect = me.toggleOnClick; } if (allowDeselect && isSelected) { me.doDeselect(record); } else { me.doSelect(record, false); } } }});