UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

782 lines (691 loc) 24.5 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() { super(); 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(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; } } if (obj1 == null && obj2 !== null) { return -1; } else if (obj2 == null && obj1 !== null) { return 1; } 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(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; } } if (obj1 == null && obj2 !== null) { return -1; } else if (obj2 == null && obj1 !== null) { return 1; } 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(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; } } if (obj1 == null && obj2 !== null) { return 1; } else if (obj2 == null && obj1 !== null) { return -1; } 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(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; } } if (obj1 == null && obj2 !== null) { return 1; } else if (obj2 == null && obj1 !== null) { return -1; } return obj1 < obj2 ? 1 : obj1 == obj2 ? 0 : -1; } }, members: { _rowArr: null, __editableColArr: null, __sortableColArr: null, __sortMethods: null, __sortColumnIndex: null, __sortAscending: null, // overridden getRowData(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(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() { 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(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(columnIndex, editable) { if (editable != this.isColumnEditable(columnIndex)) { if (this.__editableColArr == null) { this.__editableColArr = []; } this.__editableColArr[columnIndex] = editable; this.fireEvent("metaDataChanged"); } }, // overridden isColumnEditable(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(columnIndex, sortable) { if (sortable != this.isColumnSortable(columnIndex)) { if (this.__sortableColArr == null) { this.__sortableColArr = []; } this.__sortableColArr[columnIndex] = sortable; this.fireEvent("metaDataChanged"); } }, // overridden isColumnSortable(columnIndex) { return this.__sortableColArr ? this.__sortableColArr[columnIndex] !== false : true; }, // overridden sortByColumn(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(columnIndex, compare) { var methods; if (qx.lang.Type.isFunction(compare)) { methods = { ascending: compare, descending(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(columnIndex) { return this.__sortMethods[columnIndex]; }, /** * Clears the sorting. */ clearSorting() { if (this.__sortColumnIndex != -1) { this.__sortColumnIndex = -1; this.__sortAscending = true; this.fireEvent("metaDataChanged"); } }, // overridden getSortColumnIndex() { 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(columnIndex) { this.__sortColumnIndex = columnIndex; }, // overridden isSortAscending() { 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(ascending) { this.__sortAscending = ascending; }, // overridden getRowCount() { return this._rowArr.length; }, // overridden getValue(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(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(rowArr, clearSorting) { this._checkEditing(); 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() { 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(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(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(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(rowArr, startIndex, clearSorting) { this._checkEditing(); if (startIndex == null) { startIndex = 0; } // store the original length before we alter rowArr for use in splice.apply var rowArrLength = rowArr.length; // 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: startIndex + rowArrLength - 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(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(startIndex, howMany, clearSorting) { this._checkEditing(); // In the case of `removeRows`, specifically, we must create the // listeners' event data before actually removing the rows from // the row data, so that the `lastRow` calculation is correct. // If we do the delete operation first, as is done in other // methods, the final rows of the table can escape being // updated, thus leaving hanging old data on the rendered table. // This reordering (deleting after creating event data) fixes #10365. var data = { firstRow: startIndex, lastRow: this._rowArr.length - 1, firstColumn: 0, lastColumn: this.getColumnCount() - 1, removeStart: startIndex, removeCount: howMany }; this._rowArr.splice(startIndex, howMany); // Inform the listeners 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(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() { this._rowArr = this.__editableColArr = this.__sortMethods = this.__sortableColArr = null; } });