UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

783 lines (670 loc) 23.7 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) ************************************************************************ */ /** * A simple table model that provides an API for changing the model data. */ qx.Class.define("qx.ui.table.model.Simple", { extend : qx.ui.table.model.Abstract, construct : function() { this.base(arguments); this._rowArr = []; this.__sortColumnIndex = -1; // Array of objects, each with property "ascending" and "descending" this.__sortMethods = []; this.__editableColArr = null; }, properties : { /** * Whether sorting should be case sensitive */ caseSensitiveSorting : { check : "Boolean", init : true } }, statics : { /** * Default ascending sort method to use if no custom method has been * provided. * * @param row1 {var} first row * @param row2 {var} second row * @param columnIndex {Integer} the column to be sorted * @return {Integer} 1 of row1 is > row2, -1 if row1 is < row2, 0 if row1 == row2 */ _defaultSortComparatorAscending : function(row1, row2, columnIndex) { var obj1 = row1[columnIndex]; var obj2 = row2[columnIndex]; if (qx.lang.Type.isNumber(obj1) && qx.lang.Type.isNumber(obj2)) { var result = isNaN(obj1) ? isNaN(obj2) ? 0 : 1 : isNaN(obj2) ? -1 : null; if (result != null) { return result; } } return (obj1 > obj2) ? 1 : ((obj1 == obj2) ? 0 : -1); }, /** * Same as the Default ascending sort method but using case insensitivity * * @param row1 {var} first row * @param row2 {var} second row * @param columnIndex {Integer} the column to be sorted * @return {Integer} 1 of row1 is > row2, -1 if row1 is < row2, 0 if row1 == row2 */ _defaultSortComparatorInsensitiveAscending : function(row1, row2, columnIndex) { var obj1 = (row1[columnIndex].toLowerCase ? row1[columnIndex].toLowerCase() : row1[columnIndex]); var obj2 = (row2[columnIndex].toLowerCase ? row2[columnIndex].toLowerCase() : row2[columnIndex]); if (qx.lang.Type.isNumber(obj1) && qx.lang.Type.isNumber(obj2)) { var result = isNaN(obj1) ? isNaN(obj2) ? 0 : 1 : isNaN(obj2) ? -1 : null; if (result != null) { return result; } } return (obj1 > obj2) ? 1 : ((obj1 == obj2) ? 0 : -1); }, /** * Default descending sort method to use if no custom method has been * provided. * * @param row1 {var} first row * @param row2 {var} second row * @param columnIndex {Integer} the column to be sorted * @return {Integer} 1 of row1 is > row2, -1 if row1 is < row2, 0 if row1 == row2 */ _defaultSortComparatorDescending : function(row1, row2, columnIndex) { var obj1 = row1[columnIndex]; var obj2 = row2[columnIndex]; if (qx.lang.Type.isNumber(obj1) && qx.lang.Type.isNumber(obj2)) { var result = isNaN(obj1) ? isNaN(obj2) ? 0 : 1 : isNaN(obj2) ? -1 : null; if (result != null) { return result; } } return (obj1 < obj2) ? 1 : ((obj1 == obj2) ? 0 : -1); }, /** * Same as the Default descending sort method but using case insensitivity * * @param row1 {var} first row * @param row2 {var} second row * @param columnIndex {Integer} the column to be sorted * @return {Integer} 1 of row1 is > row2, -1 if row1 is < row2, 0 if row1 == row2 */ _defaultSortComparatorInsensitiveDescending : function(row1, row2, columnIndex) { var obj1 = (row1[columnIndex].toLowerCase ? row1[columnIndex].toLowerCase() : row1[columnIndex]); var obj2 = (row2[columnIndex].toLowerCase ? row2[columnIndex].toLowerCase() : row2[columnIndex]); if (qx.lang.Type.isNumber(obj1) && qx.lang.Type.isNumber(obj2)) { var result = isNaN(obj1) ? isNaN(obj2) ? 0 : 1 : isNaN(obj2) ? -1 : null; if (result != null) { return result; } } return (obj1 < obj2) ? 1 : ((obj1 == obj2) ? 0 : -1); } }, members : { _rowArr : null, __editableColArr : null, __sortableColArr : null, __sortMethods : null, __sortColumnIndex : null, __sortAscending : null, // overridden getRowData : function(rowIndex) { var rowData = this._rowArr[rowIndex]; if (rowData == null || rowData.originalData == null) { return rowData; } else { return rowData.originalData; } }, /** * Returns the data of one row as map containing the column IDs as key and * the cell values as value. Also the meta data is included. * * @param rowIndex {Integer} the model index of the row. * @return {Map} a Map containing the column values. */ getRowDataAsMap : function(rowIndex) { var rowData = this._rowArr[rowIndex]; if (rowData != null) { var map = {}; // get the current set data for (var col = 0; col < this.getColumnCount(); col++) { map[this.getColumnId(col)] = rowData[col]; } if (rowData.originalData != null) { // merge in the meta data for (var key in rowData.originalData) { if (map[key] == undefined) { map[key] = rowData.originalData[key]; } } } return map; } // may be null, which is ok return (rowData && rowData.originalData) ? rowData.originalData : null; }, /** * Gets the whole data as an array of maps. * * Note: Individual items are retrieved by {@link #getRowDataAsMap}. * @return {Map[]} Array of row data maps */ getDataAsMapArray: function() { var len = this.getRowCount(); var data = []; for (var i = 0; i < len; i++) { data.push(this.getRowDataAsMap(i)); } return data; }, /** * Sets all columns editable or not editable. * * @param editable {Boolean} whether all columns are editable. */ setEditable : function(editable) { this.__editableColArr = []; for (var col=0; col<this.getColumnCount(); col++) { this.__editableColArr[col] = editable; } this.fireEvent("metaDataChanged"); }, /** * Sets whether a column is editable. * * @param columnIndex {Integer} the column of which to set the editable state. * @param editable {Boolean} whether the column should be editable. */ setColumnEditable : function(columnIndex, editable) { if (editable != this.isColumnEditable(columnIndex)) { if (this.__editableColArr == null) { this.__editableColArr = []; } this.__editableColArr[columnIndex] = editable; this.fireEvent("metaDataChanged"); } }, // overridden isColumnEditable : function(columnIndex) { return this.__editableColArr ? (this.__editableColArr[columnIndex] == true) : false; }, /** * Sets whether a column is sortable. * * @param columnIndex {Integer} the column of which to set the sortable state. * @param sortable {Boolean} whether the column should be sortable. */ setColumnSortable : function(columnIndex, sortable) { if (sortable != this.isColumnSortable(columnIndex)) { if (this.__sortableColArr == null) { this.__sortableColArr = []; } this.__sortableColArr[columnIndex] = sortable; this.fireEvent("metaDataChanged"); } }, // overridden isColumnSortable : function(columnIndex) { return ( this.__sortableColArr ? (this.__sortableColArr[columnIndex] !== false) : true ); }, // overridden sortByColumn : function(columnIndex, ascending) { // NOTE: We use different comparators for ascending and descending, // because comparators should be really fast. var comparator; var sortMethods = this.__sortMethods[columnIndex]; if (sortMethods) { comparator = (ascending ? sortMethods.ascending : sortMethods.descending); } else { if (this.getCaseSensitiveSorting()) { comparator = (ascending ? qx.ui.table.model.Simple._defaultSortComparatorAscending : qx.ui.table.model.Simple._defaultSortComparatorDescending); } else { comparator = (ascending ? qx.ui.table.model.Simple._defaultSortComparatorInsensitiveAscending : qx.ui.table.model.Simple._defaultSortComparatorInsensitiveDescending); } } comparator.columnIndex = columnIndex; this._rowArr.sort(function(row1, row2) { return comparator(row1, row2, columnIndex); }); this.__sortColumnIndex = columnIndex; this.__sortAscending = ascending; var data = { columnIndex : columnIndex, ascending : ascending }; this.fireDataEvent("sorted", data); this.fireEvent("metaDataChanged"); }, /** * Specify the methods to use for ascending and descending sorts of a * particular column. * * @param columnIndex {Integer} * The index of the column for which the sort methods are being * provided. * * @param compare {Function|Map} * If provided as a Function, this is the comparator function to sort in * ascending order. It takes three parameters: the two arrays of row data, * row1 and row2, being compared and the column index sorting was requested * for. * * For backwards compatability, user-supplied compare functions may still * take only two parameters, the two arrays of row data, row1 and row2, * being compared and obtain the column index as arguments.callee.columnIndex. * This is deprecated, however, as arguments.callee is disallowed in ES5 strict * mode and ES6. * * The comparator function must return 1, 0 or -1, when the column in row1 * is greater than, equal to, or less than, respectively, the column in * row2. * * If this parameter is a Map, it shall have two properties: "ascending" * and "descending". The property value of each is a comparator * function, as described above. * * If only the "ascending" function is provided (i.e. this parameter is * a Function, not a Map), then the "descending" function is built * dynamically by passing the two parameters to the "ascending" function * in reversed order. <i>Use of a dynamically-built "descending" function * generates at least one extra function call for each row in the table, * and possibly many more. If the table is expected to have more than * about 1000 rows, you will likely want to provide a map with a custom * "descending" sort function as well as the "ascending" one.</i> * */ setSortMethods : function(columnIndex, compare) { var methods; if (qx.lang.Type.isFunction(compare)) { methods = { ascending : compare, descending : function(row1, row2, columnIndex) { /* assure backwards compatibility for sort functions using * arguments.callee.columnIndex and fix a bug where retreiveing * column index via this way did not work for the case where a * single comparator function was used. * Note that arguments.callee is not available in ES5 strict mode and ES6. * See discussion in * https://github.com/qooxdoo/qooxdoo/pull/9499#pullrequestreview-99655182 */ compare.columnIndex = columnIndex; return compare(row2, row1, columnIndex); } }; } else { methods = compare; } this.__sortMethods[columnIndex] = methods; }, /** * Returns the sortMethod(s) for a table column. * * @param columnIndex {Integer} The index of the column for which the sort * methods are being provided. * * @return {Map} a map with the two properties "ascending" * and "descending" for the specified column. * The property value of each is a comparator function, as described * in {@link #setSortMethods}. */ getSortMethods : function(columnIndex) { return this.__sortMethods[columnIndex]; }, /** * Clears the sorting. */ clearSorting : function() { if (this.__sortColumnIndex != -1) { this.__sortColumnIndex = -1; this.__sortAscending = true; this.fireEvent("metaDataChanged"); } }, // overridden getSortColumnIndex : function() { return this.__sortColumnIndex; }, /** * Set the sort column index * * WARNING: This should be called only by subclasses with intimate * knowledge of what they are doing! * * @param columnIndex {Integer} index of the column */ _setSortColumnIndex : function(columnIndex) { this.__sortColumnIndex = columnIndex; }, // overridden isSortAscending : function() { return this.__sortAscending; }, /** * Set whether to sort in ascending order or not. * * WARNING: This should be called only by subclasses with intimate * knowledge of what they are doing! * * @param ascending {Boolean} * <i>true</i> for an ascending sort; * <i> false</i> for a descending sort. */ _setSortAscending : function(ascending) { this.__sortAscending = ascending; }, // overridden getRowCount : function() { return this._rowArr.length; }, // overridden getValue : function(columnIndex, rowIndex) { if (rowIndex < 0 || rowIndex >= this._rowArr.length) { throw new Error("this._rowArr out of bounds: " + rowIndex + " (0.." + this._rowArr.length + ")"); } return this._rowArr[rowIndex][columnIndex]; }, // overridden setValue : function(columnIndex, rowIndex, value) { if (this._rowArr[rowIndex][columnIndex] != value) { this._rowArr[rowIndex][columnIndex] = value; // Inform the listeners if (this.hasListener("dataChanged")) { var data = { firstRow : rowIndex, lastRow : rowIndex, firstColumn : columnIndex, lastColumn : columnIndex }; this.fireDataEvent("dataChanged", data); } if (columnIndex == this.__sortColumnIndex) { this.clearSorting(); } } }, /** * Sets the whole data in a bulk. * * @param rowArr {var[][]} An array containing an array for each row. Each * row-array contains the values in that row in the order of the columns * in this model. * @param clearSorting {Boolean ? true} Whether to clear the sort state. */ setData : function(rowArr, clearSorting) { this._rowArr = rowArr; // Inform the listeners if (this.hasListener("dataChanged")) { var data = { firstRow : 0, lastRow : rowArr.length - 1, firstColumn : 0, lastColumn : this.getColumnCount() - 1 }; this.fireDataEvent("dataChanged", data); } if (clearSorting !== false) { this.clearSorting(); } }, /** * Returns the data of this model. * * Warning: Do not alter this array! If you want to change the data use * {@link #setData}, {@link #setDataAsMapArray} or {@link #setValue} instead. * * @return {var[][]} An array containing an array for each row. Each * row-array contains the values in that row in the order of the columns * in this model. */ getData : function() { return this._rowArr; }, /** * Sets the whole data in a bulk. * * @param mapArr {Map[]} An array containing a map for each row. Each * row-map contains the column IDs as key and the cell values as value. * @param rememberMaps {Boolean ? false} Whether to remember the original maps. * If true {@link #getRowData} will return the original map. * @param clearSorting {Boolean ? true} Whether to clear the sort state. */ setDataAsMapArray : function(mapArr, rememberMaps, clearSorting) { this.setData(this._mapArray2RowArr(mapArr, rememberMaps), clearSorting); }, /** * Adds some rows to the model. * * Warning: The given array will be altered! * * @param rowArr {var[][]} An array containing an array for each row. Each * row-array contains the values in that row in the order of the columns * in this model. * @param startIndex {Integer ? null} The index where to insert the new rows. If null, * the rows are appended to the end. * @param clearSorting {Boolean ? true} Whether to clear the sort state. */ addRows : function(rowArr, startIndex, clearSorting) { if (startIndex == null) { startIndex = this._rowArr.length; } // Prepare the rowArr so it can be used for apply rowArr.splice(0, 0, startIndex, 0); // Insert the new rows Array.prototype.splice.apply(this._rowArr, rowArr); // Inform the listeners var data = { firstRow : startIndex, lastRow : this._rowArr.length - 1, firstColumn : 0, lastColumn : this.getColumnCount() - 1 }; this.fireDataEvent("dataChanged", data); if (clearSorting !== false) { this.clearSorting(); } }, /** * Adds some rows to the model. * * Warning: The given array (mapArr) will be altered! * * @param mapArr {Map[]} An array containing a map for each row. Each * row-map contains the column IDs as key and the cell values as value. * @param startIndex {Integer ? null} The index where to insert the new rows. If null, * the rows are appended to the end. * @param rememberMaps {Boolean ? false} Whether to remember the original maps. * If true {@link #getRowData} will return the original map. * @param clearSorting {Boolean ? true} Whether to clear the sort state. */ addRowsAsMapArray : function(mapArr, startIndex, rememberMaps, clearSorting) { this.addRows(this._mapArray2RowArr(mapArr, rememberMaps), startIndex, clearSorting); }, /** * Sets rows in the model. The rows overwrite the old rows starting at * <code>startIndex</code> to <code>startIndex+rowArr.length</code>. * * Warning: The given array will be altered! * * @param rowArr {var[][]} An array containing an array for each row. Each * row-array contains the values in that row in the order of the columns * in this model. * @param startIndex {Integer ? null} The index where to insert the new rows. If null, * the rows are set from the beginning (0). * @param clearSorting {Boolean ? true} Whether to clear the sort state. */ setRows : function(rowArr, startIndex, clearSorting) { if (startIndex == null) { startIndex = 0; } // Prepare the rowArr so it can be used for apply rowArr.splice(0, 0, startIndex, rowArr.length); // Replace rows Array.prototype.splice.apply(this._rowArr, rowArr); // Inform the listeners var data = { firstRow : startIndex, lastRow : this._rowArr.length - 1, firstColumn : 0, lastColumn : this.getColumnCount() - 1 }; this.fireDataEvent("dataChanged", data); if (clearSorting !== false) { this.clearSorting(); } }, /** * Set rows in the model. The rows overwrite the old rows starting at * <code>startIndex</code> to <code>startIndex+rowArr.length</code>. * * Warning: The given array (mapArr) will be altered! * * @param mapArr {Map[]} An array containing a map for each row. Each * row-map contains the column IDs as key and the cell values as value. * @param startIndex {Integer ? null} The index where to insert the new rows. If null, * the rows are appended to the end. * @param rememberMaps {Boolean ? false} Whether to remember the original maps. * If true {@link #getRowData} will return the original map. * @param clearSorting {Boolean ? true} Whether to clear the sort state. */ setRowsAsMapArray : function(mapArr, startIndex, rememberMaps, clearSorting) { this.setRows(this._mapArray2RowArr(mapArr, rememberMaps), startIndex, clearSorting); }, /** * Removes some rows from the model. * * @param startIndex {Integer} the index of the first row to remove. * @param howMany {Integer} the number of rows to remove. * @param clearSorting {Boolean ? true} Whether to clear the sort state. */ removeRows : function(startIndex, howMany, clearSorting) { this._rowArr.splice(startIndex, howMany); // Inform the listeners var data = { firstRow : startIndex, lastRow : this._rowArr.length - 1, firstColumn : 0, lastColumn : this.getColumnCount() - 1, removeStart : startIndex, removeCount : howMany }; this.fireDataEvent("dataChanged", data); if (clearSorting !== false) { this.clearSorting(); } }, /** * Creates an array of maps to an array of arrays. * * @param mapArr {Map[]} An array containing a map for each row. Each * row-map contains the column IDs as key and the cell values as value. * @param rememberMaps {Boolean ? false} Whether to remember the original maps. * If true {@link #getRowData} will return the original map. * @return {var[][]} An array containing an array for each row. Each * row-array contains the values in that row in the order of the columns * in this model. */ _mapArray2RowArr : function(mapArr, rememberMaps) { var rowCount = mapArr.length; var columnCount = this.getColumnCount(); var dataArr = new Array(rowCount); var columnArr; for (var i=0; i<rowCount; ++i) { columnArr = []; if (rememberMaps) { columnArr.originalData = mapArr[i]; } for (var j=0; j<columnCount; ++j) { columnArr[j] = mapArr[i][this.getColumnId(j)]; } dataArr[i] = columnArr; } return dataArr; } }, destruct : function() { this._rowArr = this.__editableColArr = this.__sortMethods = this.__sortableColArr = null; } });