@qooxdoo/framework
Version:
The JS Framework for Coders
783 lines (670 loc) • 23.7 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 : 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;
}
});