UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

667 lines (544 loc) 17.8 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2006 STZ-IDA, Germany, http://www.stz-ida.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Til Schneider (til132) * David Perez Carmona (david-perez) ************************************************************************ */ /** * A selection model. */ qx.Class.define("qx.ui.table.selection.Model", { extend : qx.core.Object, /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ construct : function() { this.base(arguments); this.__selectedRangeArr = []; this.__anchorSelectionIndex = -1; this.__leadSelectionIndex = -1; this.hasBatchModeRefCount = 0; this.__hadChangeEventInBatchMode = false; }, /* ***************************************************************************** EVENTS ***************************************************************************** */ events: { /** Fired when the selection has changed. */ "changeSelection" : "qx.event.type.Event" }, /* ***************************************************************************** STATICS ***************************************************************************** */ statics : { /** @type {int} The selection mode "none". Nothing can ever be selected. */ NO_SELECTION : 1, /** @type {int} The selection mode "single". This mode only allows one selected item. */ SINGLE_SELECTION : 2, /** * @type {int} The selection mode "single interval". This mode only allows one * continuous interval of selected items. */ SINGLE_INTERVAL_SELECTION : 3, /** * @type {int} The selection mode "multiple interval". This mode only allows any * selection. */ MULTIPLE_INTERVAL_SELECTION : 4, /** * @type {int} The selection mode "multiple interval". This mode only allows any * selection. The difference with the previous one, is that multiple * selection is eased. A tap on an item, toggles its selection state. * On the other hand, MULTIPLE_INTERVAL_SELECTION does this behavior only * when Ctrl-tapping an item. */ MULTIPLE_INTERVAL_SELECTION_TOGGLE : 5 }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties : { /** * Set the selection mode. Valid values are {@link #NO_SELECTION}, * {@link #SINGLE_SELECTION}, {@link #SINGLE_INTERVAL_SELECTION}, * {@link #MULTIPLE_INTERVAL_SELECTION} and * {@link #MULTIPLE_INTERVAL_SELECTION_TOGGLE}. */ selectionMode : { init : 2, //SINGLE_SELECTION, check : [1,2,3,4,5], //[ NO_SELECTION, SINGLE_SELECTION, SINGLE_INTERVAL_SELECTION, MULTIPLE_INTERVAL_SELECTION, MULTIPLE_INTERVAL_SELECTION_TOGGLE ], apply : "_applySelectionMode" } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { __hadChangeEventInBatchMode : null, __anchorSelectionIndex : null, __leadSelectionIndex : null, __selectedRangeArr : null, // selectionMode property modifier _applySelectionMode : function(selectionMode) { this.resetSelection(); }, /** * * Activates / Deactivates batch mode. In batch mode, no change events will be thrown but * will be collected instead. When batch mode is turned off again and any events have * been collected, one event is thrown to inform the listeners. * * This method supports nested calling, i. e. batch mode can be turned more than once. * In this case, batch mode will not end until it has been turned off once for each * turning on. * * @param batchMode {Boolean} true to activate batch mode, false to deactivate * @return {Boolean} true if batch mode is active, false otherwise * @throws {Error} if batch mode is turned off once more than it has been turned on */ setBatchMode : function(batchMode) { if (batchMode) { this.hasBatchModeRefCount += 1; } else { if (this.hasBatchModeRefCount == 0) { throw new Error("Try to turn off batch mode althoug it was not turned on."); } this.hasBatchModeRefCount -= 1; if (this.__hadChangeEventInBatchMode) { this.__hadChangeEventInBatchMode = false; this._fireChangeSelection(); } } return this.hasBatchMode(); }, /** * * Returns whether batch mode is active. See setter for a description of batch mode. * * @return {Boolean} true if batch mode is active, false otherwise */ hasBatchMode : function() { return this.hasBatchModeRefCount > 0; }, /** * Returns the first argument of the last call to {@link #setSelectionInterval()}, * {@link #addSelectionInterval()} or {@link #removeSelectionInterval()}. * * @return {Integer} the anchor selection index. */ getAnchorSelectionIndex : function() { return this.__anchorSelectionIndex; }, /** * Sets the anchor selection index. Only use this function, if you want manipulate * the selection manually. * * @param index {Integer} the index to set. */ _setAnchorSelectionIndex : function(index) { this.__anchorSelectionIndex = index; }, /** * Returns the second argument of the last call to {@link #setSelectionInterval()}, * {@link #addSelectionInterval()} or {@link #removeSelectionInterval()}. * * @return {Integer} the lead selection index. */ getLeadSelectionIndex : function() { return this.__leadSelectionIndex; }, /** * Sets the lead selection index. Only use this function, if you want manipulate * the selection manually. * * @param index {Integer} the index to set. */ _setLeadSelectionIndex : function(index) { this.__leadSelectionIndex = index; }, /** * Returns an array that holds all the selected ranges of the table. Each * entry is a map holding information about the "minIndex" and "maxIndex" of the * selection range. * * @return {Map[]} array with all the selected ranges. */ _getSelectedRangeArr : function() { return this.__selectedRangeArr; }, /** * Resets (clears) the selection. */ resetSelection : function() { if (!this.isSelectionEmpty()) { this._resetSelection(); this._fireChangeSelection(); } }, /** * Returns whether the selection is empty. * * @return {Boolean} whether the selection is empty. */ isSelectionEmpty : function() { return this.__selectedRangeArr.length == 0; }, /** * Returns the number of selected items. * * @return {Integer} the number of selected items. */ getSelectedCount : function() { var selectedCount = 0; for (var i=0; i<this.__selectedRangeArr.length; i++) { var range = this.__selectedRangeArr[i]; selectedCount += range.maxIndex - range.minIndex + 1; } return selectedCount; }, /** * Returns whether an index is selected. * * @param index {Integer} the index to check. * @return {Boolean} whether the index is selected. */ isSelectedIndex : function(index) { for (var i=0; i<this.__selectedRangeArr.length; i++) { var range = this.__selectedRangeArr[i]; if (index >= range.minIndex && index <= range.maxIndex) { return true; } } return false; }, /** * Returns the selected ranges as an array. Each array element has a * <code>minIndex</code> and a <code>maxIndex</code> property. * * @return {Map[]} the selected ranges. */ getSelectedRanges : function() { // clone the selection array and the individual elements - this prevents the // caller from messing with the internal model var retVal = []; for (var i=0; i<this.__selectedRangeArr.length; i++) { retVal.push( { minIndex : this.__selectedRangeArr[i].minIndex, maxIndex : this.__selectedRangeArr[i].maxIndex }); } return retVal; }, /** * Calls an iterator function for each selected index. * * Usage Example: * <pre class='javascript'> * var selectedRowData = []; * mySelectionModel.iterateSelection(function(index) { * selectedRowData.push(myTableModel.getRowData(index)); * }); * </pre> * * @param iterator {Function} the function to call for each selected index. * Gets the current index as parameter. * @param object {var ? null} the object to use when calling the handler. * (this object will be available via "this" in the iterator) */ iterateSelection : function(iterator, object) { for (var i=0; i<this.__selectedRangeArr.length; i++) { for (var j=this.__selectedRangeArr[i].minIndex; j<=this.__selectedRangeArr[i].maxIndex; j++) { iterator.call(object, j); } } }, /** * Sets the selected interval. This will clear the former selection. * * @param fromIndex {Integer} the first index of the selection (including). * @param toIndex {Integer} the last index of the selection (including). */ setSelectionInterval : function(fromIndex, toIndex) { var me = this.self(arguments); switch(this.getSelectionMode()) { case me.NO_SELECTION: return; case me.SINGLE_SELECTION: // Ensure there is actually a change of selection if (this.isSelectedIndex(toIndex)) { return; } fromIndex = toIndex; break; case me.MULTIPLE_INTERVAL_SELECTION_TOGGLE: this.setBatchMode(true); try { for (var i = fromIndex; i <= toIndex; i++) { if (!this.isSelectedIndex(i)) { this._addSelectionInterval(i, i); } else { this.removeSelectionInterval(i, i); } } } catch (e) { throw e; } finally { this.setBatchMode(false); } this._fireChangeSelection(); return; } this._resetSelection(); this._addSelectionInterval(fromIndex, toIndex); this._fireChangeSelection(); }, /** * Adds a selection interval to the current selection. * * @param fromIndex {Integer} the first index of the selection (including). * @param toIndex {Integer} the last index of the selection (including). */ addSelectionInterval : function(fromIndex, toIndex) { var SelectionModel = qx.ui.table.selection.Model; switch(this.getSelectionMode()) { case SelectionModel.NO_SELECTION: return; case SelectionModel.MULTIPLE_INTERVAL_SELECTION: case SelectionModel.MULTIPLE_INTERVAL_SELECTION_TOGGLE: this._addSelectionInterval(fromIndex, toIndex); this._fireChangeSelection(); break; default: this.setSelectionInterval(fromIndex, toIndex); break; } }, /** * Removes an interval from the current selection. * * @param fromIndex {Integer} the first index of the interval (including). * @param toIndex {Integer} the last index of the interval (including). * @param rowsRemoved {Boolean?} rows were removed that caused this selection to change. * If rows were removed, move the selections over so the same rows are selected as before. */ removeSelectionInterval : function(fromIndex, toIndex, rowsRemoved) { this.__anchorSelectionIndex = fromIndex; this.__leadSelectionIndex = toIndex; var minIndex = Math.min(fromIndex, toIndex); var maxIndex = Math.max(fromIndex, toIndex); var removeCount = maxIndex + 1 - minIndex; // Crop the affected ranges var newRanges = []; var extraRange = null; for (var i=0; i<this.__selectedRangeArr.length; i++) { var range = this.__selectedRangeArr[i]; if (range.minIndex > maxIndex) { if (rowsRemoved) { // Move whole selection up. range.minIndex -= removeCount; range.maxIndex -= removeCount; } } else if (range.maxIndex >= minIndex) { // This range is affected var minIsIn = (range.minIndex >= minIndex); var maxIsIn = (range.maxIndex >= minIndex) && (range.maxIndex <= maxIndex); if (minIsIn && maxIsIn) { // This range is removed completely range = null; } else if (minIsIn) { if (rowsRemoved) { range.minIndex = minIndex; range.maxIndex -= removeCount; } else { // The range is cropped from the left range.minIndex = maxIndex + 1; } } else if (maxIsIn) { // The range is cropped from the right range.maxIndex = minIndex - 1; } else { if (rowsRemoved) { range.maxIndex -= removeCount; } else { // The range is split extraRange = { minIndex: maxIndex + 1, maxIndex: range.maxIndex }; range.maxIndex = minIndex - 1; } } } if (range) { newRanges.push(range); range = null; } if (extraRange) { newRanges.push(extraRange); extraRange = null; } } this.__selectedRangeArr = newRanges; this._fireChangeSelection(); }, /** * Resets (clears) the selection, but doesn't inform the listeners. */ _resetSelection : function() { this.__selectedRangeArr = []; this.__anchorSelectionIndex = -1; this.__leadSelectionIndex = -1; }, /** * Adds a selection interval to the current selection, but doesn't inform * the listeners. * * @param fromIndex {Integer} the first index of the selection (including). * @param toIndex {Integer} the last index of the selection (including). */ _addSelectionInterval : function(fromIndex, toIndex) { this.__anchorSelectionIndex = fromIndex; this.__leadSelectionIndex = toIndex; var minIndex = Math.min(fromIndex, toIndex); var maxIndex = Math.max(fromIndex, toIndex); // Find the index where the new range should be inserted var newRangeIndex = 0; for (;newRangeIndex<this.__selectedRangeArr.length; newRangeIndex++) { var range = this.__selectedRangeArr[newRangeIndex]; if (range.minIndex > minIndex) { break; } } // Add the new range this.__selectedRangeArr.splice(newRangeIndex, 0, { minIndex : minIndex, maxIndex : maxIndex }); // Merge overlapping ranges var lastRange = this.__selectedRangeArr[0]; for (var i=1; i<this.__selectedRangeArr.length; i++) { var range = this.__selectedRangeArr[i]; if (lastRange.maxIndex + 1 >= range.minIndex) { // The ranges are overlapping -> merge them lastRange.maxIndex = Math.max(lastRange.maxIndex, range.maxIndex); // Remove the current range this.__selectedRangeArr.splice(i, 1); // Check this index another time i--; } else { lastRange = range; } } }, // this._dumpRanges(); /** * Logs the current ranges for debug purposes. * */ _dumpRanges : function() { var text = "Ranges:"; for (var i=0; i<this.__selectedRangeArr.length; i++) { var range = this.__selectedRangeArr[i]; text += " [" + range.minIndex + ".." + range.maxIndex + "]"; } this.debug(text); }, /** * Fires the "changeSelection" event to all registered listeners. If the selection model * currently is in batch mode, only one event will be thrown when batch mode is ended. * */ _fireChangeSelection : function() { if (this.hasBatchMode()) { // In batch mode, remember event but do not throw (yet) this.__hadChangeEventInBatchMode = true; } else { // If not in batch mode, throw event this.fireEvent("changeSelection"); } } }, /* ***************************************************************************** DESTRUCTOR ***************************************************************************** */ destruct : function() { this.__selectedRangeArr = null; } });