@qooxdoo/framework
Version:
The JS Framework for Coders
782 lines (691 loc) • 24.5 kB
JavaScript
/* ************************************************************************
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;
}
});