X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/selection/Model.js diff --git a/src/selection/Model.js b/src/selection/Model.js new file mode 100644 index 00000000..023eec0a --- /dev/null +++ b/src/selection/Model.js @@ -0,0 +1,583 @@ +/** + * @class Ext.selection.Model + * @extends Ext.util.Observable + * + * Tracks what records are currently selected in a databound widget. + * + * This is an abstract class and is not meant to be directly used. + * + * DataBound UI widgets such as GridPanel, TreePanel, and ListView + * should subclass AbstractStoreSelectionModel and provide a way + * to binding to the component. + * + * The abstract methods onSelectChange and onLastFocusChanged should + * be implemented in these subclasses to update the UI widget. + */ +Ext.define('Ext.selection.Model', { + extend: 'Ext.util.Observable', + alternateClassName: 'Ext.AbstractStoreSelectionModel', + requires: ['Ext.data.StoreManager'], + // lastSelected + + /** + * @cfg {String} mode + * Modes of selection. + * Valid values are SINGLE, SIMPLE, and MULTI. Defaults to 'SINGLE' + */ + + /** + * @cfg {Boolean} allowDeselect + * Allow users to deselect a record in a DataView, List or Grid. Only applicable when the SelectionModel's mode is 'SINGLE'. Defaults to false. + */ + allowDeselect: false, + + /** + * @property selected + * READ-ONLY A MixedCollection that maintains all of the currently selected + * records. + */ + selected: null, + + + /** + * Prune records when they are removed from the store from the selection. + * This is a private flag. For an example of its usage, take a look at + * Ext.selection.TreeModel. + * @private + */ + pruneRemoved: true, + + constructor: function(cfg) { + var me = this; + + cfg = cfg || {}; + Ext.apply(me, cfg); + + me.addEvents( + /** + * @event selectionchange + * Fired after a selection change has occurred + * @param {Ext.selection.Model} this + * @param {Array} selected The selected records + */ + 'selectionchange' + ); + + me.modes = { + SINGLE: true, + SIMPLE: true, + MULTI: true + }; + + // sets this.selectionMode + me.setSelectionMode(cfg.mode || me.mode); + + // maintains the currently selected records. + me.selected = Ext.create('Ext.util.MixedCollection'); + + me.callParent(arguments); + }, + + // binds the store to the selModel. + bind : function(store, initial){ + var me = this; + + if(!initial && me.store){ + if(store !== me.store && me.store.autoDestroy){ + me.store.destroy(); + }else{ + me.store.un("add", me.onStoreAdd, me); + me.store.un("clear", me.onStoreClear, me); + me.store.un("remove", me.onStoreRemove, me); + me.store.un("update", me.onStoreUpdate, me); + } + } + if(store){ + store = Ext.data.StoreManager.lookup(store); + store.on({ + add: me.onStoreAdd, + clear: me.onStoreClear, + remove: me.onStoreRemove, + update: me.onStoreUpdate, + scope: me + }); + } + me.store = store; + if(store && !initial) { + me.refresh(); + } + }, + + selectAll: function(silent) { + var selections = this.store.getRange(), + i = 0, + len = selections.length; + + for (; i < len; i++) { + this.doSelect(selections[i], true, silent); + } + }, + + deselectAll: function() { + var selections = this.getSelection(), + i = 0, + len = selections.length; + + for (; i < len; i++) { + this.doDeselect(selections[i]); + } + }, + + // 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; + + switch (me.selectionMode) { + case 'MULTI': + if (e.ctrlKey && me.isSelected(record)) { + me.doDeselect(record, false); + } else if (e.shiftKey && me.lastFocused) { + me.selectRange(me.lastFocused, record, e.ctrlKey); + } else if (e.ctrlKey) { + me.doSelect(record, true, false); + } else if (me.isSelected(record) && !e.shiftKey && !e.ctrlKey && me.selected.getCount() > 1) { + me.doSelect(record, false, false); + } else { + me.doSelect(record, false); + } + break; + case 'SIMPLE': + if (me.isSelected(record)) { + me.doDeselect(record); + } else { + me.doSelect(record, true); + } + break; + case 'SINGLE': + // if allowDeselect is on and this record isSelected, deselect it + if (me.allowDeselect && me.isSelected(record)) { + me.doDeselect(record); + // select the record and do NOT maintain existing selections + } else { + me.doSelect(record, false); + } + break; + } + }, + + /** + * 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, dir){ + var me = this, + store = me.store, + selectedCount = 0, + i, + tmp, + dontDeselect, + records = []; + + if (me.isLocked()){ + return; + } + + if (!keepExisting) { + me.clearSelections(); + } + + if (!Ext.isNumber(startRow)) { + startRow = store.indexOf(startRow); + } + if (!Ext.isNumber(endRow)) { + endRow = store.indexOf(endRow); + } + + // swap values + if (startRow > endRow){ + tmp = endRow; + endRow = startRow; + startRow = tmp; + } + + for (i = startRow; i <= endRow; i++) { + if (me.isSelected(store.getAt(i))) { + selectedCount++; + } + } + + if (!dir) { + dontDeselect = -1; + } else { + dontDeselect = (dir == 'up') ? startRow : endRow; + } + + for (i = startRow; i <= endRow; i++){ + if (selectedCount == (endRow - startRow + 1)) { + if (i != dontDeselect) { + me.doDeselect(i, true); + } + } else { + records.push(store.getAt(i)); + } + } + me.doMultiSelect(records, true); + }, + + /** + * Selects a record instance by record instance or index. + * @param {Ext.data.Model/Index} records An array of records or an index + * @param {Boolean} keepExisting + * @param {Boolean} suppressEvent Set to false to not fire a select event + */ + select: function(records, keepExisting, suppressEvent) { + this.doSelect(records, keepExisting, suppressEvent); + }, + + /** + * Deselects a record instance by record instance or index. + * @param {Ext.data.Model/Index} records An array of records or an index + * @param {Boolean} suppressEvent Set to false 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) { + return; + } + if (typeof records === "number") { + records = [me.store.getAt(records)]; + } + if (me.selectionMode == "SINGLE" && records) { + record = records.length ? records[0] : records; + me.doSingleSelect(record, suppressEvent); + } else { + me.doMultiSelect(records, keepExisting, suppressEvent); + } + }, + + doMultiSelect: function(records, keepExisting, suppressEvent) { + var me = this, + selected = me.selected, + change = false, + i = 0, + len, record; + + if (me.locked) { + return; + } + + + records = !Ext.isArray(records) ? [records] : records; + len = records.length; + if (!keepExisting && selected.getCount() > 0) { + change = true; + me.doDeselect(me.getSelection(), true); + } + + for (; i < len; i++) { + record = records[i]; + if (keepExisting && me.isSelected(record)) { + continue; + } + change = true; + me.lastSelected = record; + selected.add(record); + + me.onSelectChange(record, true, suppressEvent); + } + me.setLastFocused(record, suppressEvent); + // fire selchange if there was a change and there is no suppressEvent flag + me.maybeFireSelectionChange(change && !suppressEvent); + }, + + // records can be an index, a record or an array of records + doDeselect: function(records, suppressEvent) { + var me = this, + selected = me.selected, + change = false, + i = 0, + len, record; + + if (me.locked) { + return; + } + + if (typeof records === "number") { + records = [me.store.getAt(records)]; + } + + records = !Ext.isArray(records) ? [records] : records; + len = records.length; + for (; i < len; i++) { + record = records[i]; + if (selected.remove(record)) { + if (me.lastSelected == record) { + me.lastSelected = selected.last(); + } + me.onSelectChange(record, false, suppressEvent); + change = true; + } + } + // fire selchange if there was a change and there is no suppressEvent flag + me.maybeFireSelectionChange(change && !suppressEvent); + }, + + doSingleSelect: function(record, suppressEvent) { + var me = this, + selected = me.selected; + + if (me.locked) { + return; + } + // already selected. + // should we also check beforeselect? + if (me.isSelected(record)) { + return; + } + if (selected.getCount() > 0) { + me.doDeselect(me.lastSelected, suppressEvent); + } + selected.add(record); + me.lastSelected = record; + me.onSelectChange(record, true, suppressEvent); + if (!suppressEvent) { + me.setLastFocused(record); + } + me.maybeFireSelectionChange(!suppressEvent); + }, + + /** + * @param {Ext.data.Model} record + * Set a record as the last focused record. This does NOT mean + * that the record has been selected. + */ + setLastFocused: function(record, supressFocus) { + var me = this, + recordBeforeLast = me.lastFocused; + me.lastFocused = record; + me.onLastFocusChanged(recordBeforeLast, record, supressFocus); + }, + + /** + * Determines if this record is currently focused. + * @param Ext.data.Record record + */ + isFocused: function(record) { + return record === this.getLastFocused(); + }, + + + // fire selection change as long as true is not passed + // into maybeFireSelectionChange + maybeFireSelectionChange: function(fireEvent) { + if (fireEvent) { + var me = this; + me.fireEvent('selectionchange', me, me.getSelection()); + } + }, + + /** + * Returns the last selected record. + */ + getLastSelected: function() { + return this.lastSelected; + }, + + getLastFocused: function() { + return this.lastFocused; + }, + + /** + * Returns an array of the currently selected records. + */ + getSelection: function() { + return this.selected.getRange(); + }, + + /** + * Returns the current selectionMode. SINGLE, MULTI or SIMPLE. + */ + getSelectionMode: function() { + return this.selectionMode; + }, + + /** + * Sets the current selectionMode. 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 + */ + setLocked: function(locked) { + this.locked = !!locked; + }, + + /** + * Returns true if the specified row is selected. + * @param {Record/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.indexOf(record) !== -1; + }, + + /** + * Returns true if there is a selected record. + * @return {Boolean} + */ + hasSelection: function() { + return this.selected.getCount() > 0; + }, + + refresh: function() { + var me = this, + toBeSelected = [], + oldSelections = me.getSelection(), + len = oldSelections.length, + selection, + change, + i = 0, + lastFocused = this.getLastFocused(); + + // check to make sure that there are no records + // missing after the refresh was triggered, prune + // them from what is to be selected if so + for (; i < len; i++) { + selection = oldSelections[i]; + if (!this.pruneRemoved || me.store.indexOf(selection) !== -1) { + toBeSelected.push(selection); + } + } + + // there was a change from the old selected and + // the new selection + if (me.selected.getCount() != toBeSelected.length) { + change = true; + } + + me.clearSelections(); + + if (me.store.indexOf(lastFocused) !== -1) { + // restore the last focus but supress restoring focus + this.setLastFocused(lastFocused, true); + } + + if (toBeSelected.length) { + // perform the selection again + me.doSelect(toBeSelected, false, true); + } + + me.maybeFireSelectionChange(change); + }, + + clearSelections: function() { + // reset the entire selection to nothing + var me = this; + me.selected.clear(); + me.lastSelected = null; + me.setLastFocused(null); + }, + + // when a record is added to a store + onStoreAdd: function() { + + }, + + // when a store is cleared remove all selections + // (if there were any) + onStoreClear: function() { + var me = this, + selected = this.selected; + + if (selected.getCount > 0) { + selected.clear(); + me.lastSelected = null; + me.setLastFocused(null); + me.maybeFireSelectionChange(true); + } + }, + + // prune records from the SelectionModel if + // they were selected at the time they were + // removed. + onStoreRemove: function(store, record) { + var me = this, + selected = me.selected; + + if (me.locked || !me.pruneRemoved) { + return; + } + + if (selected.remove(record)) { + if (me.lastSelected == record) { + me.lastSelected = null; + } + if (me.getLastFocused() == record) { + me.setLastFocused(null); + } + me.maybeFireSelectionChange(true); + } + }, + + getCount: function() { + return this.selected.getCount(); + }, + + // cleanup. + destroy: function() { + + }, + + // if records are updated + onStoreUpdate: function() { + + }, + + // @abstract + onSelectChange: function(record, isSelected, suppressEvent) { + + }, + + // @abstract + onLastFocusChanged: function(oldFocused, newFocused) { + + }, + + // @abstract + onEditorKey: function(field, e) { + + }, + + // @abstract + bindComponent: function(cmp) { + + } +}); \ No newline at end of file