/** * Tracks what records are currently selected in a databound widget. This class is mixed in to * {@link Ext.view.View dataview} and all subclasses. * @private */Ext.define('Ext.mixin.Selectable', { extend: 'Ext.Mixin', mixinConfig: { id: 'selectable', after: { updateStore: 'updateStore' } }, /** * @event selectionchange * Fires when a selection changes. * @param {Ext.mixin.Selectable} this * @param {Ext.data.Model[]} records The records whose selection has changed. */ config: { /** * @cfg {Boolean} disableSelection * Set to `true` to disable selection. * This configuration will lock the selection model that the DataView uses. * @accessor */ disableSelection: null, /** * @cfg {'SINGLE'/'SIMPLE'/'MULTI'} mode * Modes of selection. * @accessor */ mode: 'SINGLE', /** * @cfg {Boolean} allowDeselect * Allow users to deselect a record in a DataView, List or Grid. Only applicable when * the Selectable's `mode` is `'SINGLE'`. * @accessor */ allowDeselect: false, /** * @cfg {Ext.data.Model} lastSelected * @private * @accessor */ lastSelected: null, /** * @cfg {Ext.data.Model} lastFocused * @private * @accessor */ lastFocused: null, /** * @cfg {Boolean} deselectOnContainerClick * Set to `true` to deselect current selection when the container body is clicked. * @accessor */ deselectOnContainerClick: true, /** * @cfg {Ext.util.Collection} selected * A {@link Ext.util.Collection} instance, or configuration object used to create * the collection of selected records. * @readonly */ selected: true, /** * @cfg {Boolean} pruneRemoved * 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, /** * @cfg {Ext.data.Model} selection * The selected record. */ selection: null, /** * @cfg twoWayBindable * @inheritdoc Ext.mixin.Bindable#cfg-twoWayBindable */ twoWayBindable: { selection: 1 }, /** * @cfg publishes * @inheritdoc Ext.mixin.Bindable#cfg-publishes */ publishes: { selection: 1 } }, modes: { SINGLE: true, SIMPLE: true, MULTI: true }, onNavigate: function(event) { }, selectableEventHooks: { add: 'onSelectionStoreAdd', remove: 'onSelectionStoreRemove', update: 'onSelectionStoreUpdate', clear: { fn: 'onSelectionStoreClear', priority: 1000 }, load: 'refreshSelection', refresh: 'refreshSelection' }, initSelectable: function() { this.publishState('selection', this.getSelection()); }, applySelected: function(selected) { if (!selected.isCollection) { selected = new Ext.util.Collection(selected); } // Add this Selectable as an observer immediately so that we are informed of any // mutations which occur in this event run. selected.addObserver(this); return selected; }, /** * @private */ applyMode: function(mode) { mode = mode ? mode.toUpperCase() : 'SINGLE'; // set to mode specified unless it doesnt exist, in that case // use single. return this.modes[mode] ? mode : 'SINGLE'; }, /** * @private */ updateStore: function(newStore, oldStore) { var me = this, bindEvents = Ext.apply({}, me.selectableEventHooks, { scope: me }); if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) { if (oldStore.autoDestroy) { oldStore.destroy(); } else { oldStore.un(bindEvents); } } if (newStore) { newStore.on(bindEvents); me.refreshSelection(); } }, /** * Selects all records. * @param {Boolean} silent `true` to suppress all select events. */ selectAll: function(silent) { var me = this, selections = me.getStore().getRange(); me.select(selections, true, silent); }, /** * Deselects all records. */ deselectAll: function(supress) { var me = this; me.deselect(me.getSelected().getRange(), supress); me.setLastSelected(null); me.setLastFocused(null); }, updateSelection: function(selection) { if (this.changingSelection) { return; } if (selection) { this.select(selection); } else { this.deselectAll(); } }, // Provides differentiation of logic between MULTI, SIMPLE and SINGLE // selection modes. selectWithEvent: function(record) { var me = this, isSelected = me.isSelected(record); switch (me.getMode()) { case 'MULTI': case 'SIMPLE': if (isSelected) { me.deselect(record); } else { me.select(record, true); } break; case 'SINGLE': if (me.getAllowDeselect() && isSelected) { // if allowDeselect is on and this record isSelected, deselect it me.deselect(record); } else { // select the record and do NOT maintain existing selections me.select(record, false); } break; } }, /** * Selects a range of rows if the selection model * {@link Ext.mixin.Selectable#getDisableSelection} is not locked. * All rows in between `startRecord` and `endRecord` are also selected. * @param {Number} startRecord The index of the first row in the range. * @param {Number} endRecord The index of the last row in the range. * @param {Boolean} [keepExisting] `true` to retain existing selections. */ selectRange: function(startRecord, endRecord, keepExisting) { var me = this, store = me.getStore(), records = [], tmp, i; if (me.getDisableSelection()) { return; } // swap values if (startRecord > endRecord) { tmp = endRecord; endRecord = startRecord; startRecord = tmp; } for (i = startRecord; i <= endRecord; i++) { records.push(store.getAt(i)); } this.doMultiSelect(records, keepExisting); }, /** * Adds the given records to the currently selected set. * @param {Ext.data.Model/Array/Number} records The records to select. * @param {Boolean} keepExisting If `true`, the existing selection will be added to * (if not, the old selection is replaced). * @param {Boolean} suppressEvent If `true`, the `select` event will not be fired. */ select: function(records, keepExisting, suppressEvent) { var me = this, record; if (me.getDisableSelection()) { return; } if (typeof records === "number") { records = [me.getStore().getAt(records)]; } if (!records) { return; } if (me.getMode() === "SINGLE" && records) { record = records.length ? records[0] : records; me.doSingleSelect(record, suppressEvent); } else { me.doMultiSelect(records, keepExisting, suppressEvent); } }, /** * Selects a single record. * @private */ doSingleSelect: function(record, suppressEvent) { this.doMultiSelect([record], false, suppressEvent); }, /** * Selects a set of multiple records. * @private */ doMultiSelect: function(records, keepExisting, suppressEvent) { if (records === null || this.getDisableSelection()) { return; } records = !Ext.isArray(records) ? [records] : records; // eslint-disable-next-line vars-on-top var me = this, selected = me.getSelected(), selectionCount = selected.getCount(), store = me.getStore(), toRemove = [], record, i, len; if (!keepExisting && selectionCount) { toRemove = selected.getRange(); } // Ensure they are all records for (i = 0, len = records.length; i < len; i++) { record = records[i]; if (typeof record === 'number') { records[i] = store.getAt(record); } } // Potentially remove from, then add the selected Collection. // We will react to successful removal as an observer. // We will need to know at that time whether the event is suppressed. selected.suppressEvent = suppressEvent; selected.splice(selectionCount, toRemove, records); selected.suppressEvent = false; }, /** * Deselects the given record(s). If many records are currently selected, it will only deselect * those you pass in. * @param {Number/Array/Ext.data.Model} records The record(s) to deselect. Can also be a number * to reference by index. * @param {Boolean} suppressEvent If `true` the `deselect` event will not be fired. */ deselect: function(records, suppressEvent) { var me = this, selected, store, record, i, len; if (me.getDisableSelection()) { return; } records = Ext.isArray(records) ? records : [records]; selected = me.getSelected(); store = me.getStore(); // Ensure they are all records for (i = 0, len = records.length; i < len; i++) { record = records[i]; if (typeof record === 'number') { records[i] = store.getAt(record); } } // Remove the records from the selected Collection. // We will react to successful removal as an observer. // We will need to know at that time whether the event is suppressed. selected.suppressEvent = suppressEvent; selected.remove(records); selected.suppressEvent = false; }, /** * @private * Respond to deselection. Call the onItemDeselect template method */ onCollectionRemove: function(selectedCollection, chunk) { var me = this, lastSelected = me.getLastSelected(), records = chunk.items; // Keep lastSelected up to date if (lastSelected && !selectedCollection.contains(lastSelected)) { me.setLastSelected(selectedCollection.last()); } me.onItemDeselect(records, selectedCollection.suppressEvent); if (!selectedCollection.suppressEvent) { me.fireSelectionChange(records); } }, /** * @private * Respond to selection. Call the onItemSelect template method */ onCollectionAdd: function(selectedCollection, adds) { var me = this, records = adds.items; // Keep lastSelected up to date me.setLastSelected(selectedCollection.last()); me.onItemSelect(records, selectedCollection.suppressEvent); if (!selectedCollection.suppressEvent) { me.fireSelectionChange(records); } }, // TODO: This is the job of a NavigationModel /** * Sets a record as the last focused record. This does NOT mean * that the record has been selected. * @param {Ext.data.Record} newRecord * @param {Ext.data.Record} oldRecord */ updateLastFocused: function(newRecord, oldRecord) { this.onLastFocusChanged(oldRecord, newRecord); }, fireSelectionChange: function(records) { var me = this; me.changingSelection = true; me.setSelection(me.getLastSelected() || null); me.changingSelection = false; me.fireAction('selectionchange', [me, records], 'getSelections'); }, /** * Returns the currently selected records. * @return {Ext.data.Model[]} The selected records. */ getSelections: function() { return this.getSelected().getRange(); }, /** * 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.getStore().getAt(record) : record; return this.getSelected().indexOf(record) !== -1; }, /** * Returns `true` if there is a selected record. * @return {Boolean} */ hasSelection: function() { return this.getSelected().getCount() > 0; }, /** * @private */ refreshSelection: function() { var me = this, selected = me.getSelected(), selections = selected.getRange(), selectionLength = selections.length, storeCollection = me.getStore().getData(), toDeselect = [], toReselect = [], i, rec, matchingSelection; // Build the toDeselect list if (me.getPruneRemoved()) { // Uncover the unfiltered selection if it's there. // We only want to prune from the selection records whhich are // *really* no longer in the store. storeCollection = storeCollection.getSource() || storeCollection; for (i = 0; i < selectionLength; i++) { rec = selections[i]; matchingSelection = storeCollection.get(storeCollection.getKey(rec)); if (matchingSelection) { if (matchingSelection !== rec) { toDeselect.push(rec); toReselect.push(matchingSelection); } } else { toDeselect.push(rec); } } } // Update the selected Collection. // Records which are no longer present will be in the toDeselect list // Records which have the same id which have returned will be in the toSelect list. // We will react to successful removal as an observer. // We will need to know at that time whether the event is suppressed. selected.suppressEvent = true; selected.splice(selected.getCount(), toDeselect, toReselect); selected.suppressEvent = false; }, // prune records from the SelectionModel if // they were selected at the time they were // removed. onSelectionStoreRemove: function(store, records) { var me = this, selected = me.getSelected(), ln = records.length, removed, record, i; if (me.getDisableSelection()) { return; } for (i = 0; i < ln; i++) { record = records[i]; if (selected.remove(record)) { if (me.getLastSelected() == record) { // eslint-disable-line eqeqeq me.setLastSelected(null); } if (me.getLastFocused() == record) { // eslint-disable-line eqeqeq me.setLastFocused(null); } removed = removed || []; removed.push(record); } } if (removed) { me.fireSelectionChange([removed]); } }, onSelectionStoreClear: function(store) { var records = store.getData().items; this.onSelectionStoreRemove(store, records); }, /** * Returns the number of selections. * @return {Number} */ getSelectionCount: function() { return this.getSelected().getCount(); }, onSelectionStoreAdd: Ext.emptyFn, onSelectionStoreUpdate: Ext.emptyFn, onItemSelect: Ext.emptyFn, onItemDeselect: Ext.emptyFn, onLastFocusChanged: Ext.emptyFn, onEditorKey: Ext.emptyFn}, function() { /** * Selects a record instance by record instance or index. * @member Ext.mixin.Selectable * @method doSelect * @param {Ext.data.Model/Number} records An array of records or an index. * @param {Boolean} keepExisting * @param {Boolean} suppressEvent Set to `false` to not fire a select event. * @deprecated 2.0.0 Please use {@link #select} instead. */ /** * Deselects a record instance by record instance or index. * @member Ext.mixin.Selectable * @method doDeselect * @param {Ext.data.Model/Number} records An array of records or an index. * @param {Boolean} suppressEvent Set to `false` to not fire a deselect event. * @deprecated 2.0.0 Please use {@link #deselect} instead. */ /** * Returns the selection mode currently used by this Selectable. * @member Ext.mixin.Selectable * @method getSelectionMode * @return {String} The current mode. * @deprecated 2.0.0 Please use {@link #getMode} instead. */ /** * Returns the array of previously selected items. * @member Ext.mixin.Selectable * @method getLastSelected * @return {Array} The previous selection. * @deprecated 2.0.0 This method is deprecated. */ /** * Returns `true` if the Selectable is currently locked. * @member Ext.mixin.Selectable * @method isLocked * @return {Boolean} True if currently locked * @deprecated 2.0.0 Please use {@link #getDisableSelection} instead. */ /** * This was an internal function accidentally exposed in 1.x and now deprecated. Calling it * has no effect * @member Ext.mixin.Selectable * @method setLastFocused * @deprecated 2.0.0 This method is deprecated. */ /** * Deselects any currently selected records and clears all stored selections. * @member Ext.mixin.Selectable * @method clearSelections * @deprecated 2.0.0 Please use {@link #deselectAll} instead. */ /** * Returns the number of selections. * @member Ext.mixin.Selectable * @method getCount * @return {Number} * @deprecated 2.0.0 Please use {@link #getSelectionCount} instead. */ /** * @cfg locked * @inheritdoc Ext.mixin.Selectable#cfg-disableSelection * @deprecated 2.0.0 Please use {@link #disableSelection} instead. */});