@qooxdoo/framework
Version:
The JS Framework for Coders
739 lines (602 loc) • 20.4 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)
* Fabian Jakobs (fjakobs)
************************************************************************ */
/**
* The table pane that shows a certain section from a table. This class handles
* the display of the data part of a table and is therefore the base for virtual
* scrolling.
*/
qx.Class.define("qx.ui.table.pane.Pane",
{
extend : qx.ui.core.Widget,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param paneScroller {qx.ui.table.pane.Scroller} the TablePaneScroller the header belongs to.
*/
construct : function(paneScroller)
{
this.base(arguments);
this.__paneScroller = paneScroller;
this.__lastColCount = 0;
this.__lastRowCount = 0;
this.__rowCache = [];
},
/*
*****************************************************************************
EVENTS
*****************************************************************************
*/
events :
{
/**
* Whether the current view port of the pane has not loaded data.
* The data object of the event indicates if the table pane has to reload
* data or not. Can be used to give the user feedback of the loading state
* of the rows.
*/
"paneReloadsData" : "qx.event.type.Data",
/**
* Whenever the content of the table pane has been updated (rendered)
* trigger a paneUpdated event. This allows the canvas cellrenderer to act
* once the new cells have been integrated in the dom.
*/
"paneUpdated" : "qx.event.type.Event"
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
/** The index of the first row to show. */
firstVisibleRow :
{
check : "Number",
init : 0,
apply : "_applyFirstVisibleRow"
},
/** The number of rows to show. */
visibleRowCount :
{
check : "Number",
init : 0,
apply : "_applyVisibleRowCount"
},
/**
* Maximum number of cached rows. If the value is <code>-1</code> the cache
* size is unlimited
*/
maxCacheLines :
{
check : "Number",
init : 1000,
apply : "_applyMaxCacheLines"
},
// overridden
allowShrinkX :
{
refine : true,
init : false
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__lastRowCount : null,
__lastColCount : null,
__paneScroller : null,
__tableContainer : null,
__focusedRow : null,
__focusedCol : null,
// sparse array to cache rendered rows
__rowCache : null,
__rowCacheCount : 0,
// property modifier
_applyFirstVisibleRow : function(value, old) {
this.updateContent(false, value-old);
},
// property modifier
_applyVisibleRowCount : function(value, old) {
this.updateContent(true);
},
// overridden
_getContentHint : function()
{
// the preferred height is 400 pixel. We don't use rowCount * rowHeight
// because this is typically too large.
return {
width: this.getPaneScroller().getTablePaneModel().getTotalWidth(),
height: 400
};
},
/**
* Returns the TablePaneScroller this pane belongs to.
*
* @return {qx.ui.table.pane.Scroller} the TablePaneScroller.
*/
getPaneScroller : function() {
return this.__paneScroller;
},
/**
* Returns the table this pane belongs to.
*
* @return {qx.ui.table.Table} the table.
*/
getTable : function() {
return this.__paneScroller.getTable();
},
/**
* Sets the currently focused cell.
*
* @param col {Integer?null} the model index of the focused cell's column.
* @param row {Integer?null} the model index of the focused cell's row.
* @param massUpdate {Boolean ? false} Whether other updates are planned as well.
* If true, no repaint will be done.
*/
setFocusedCell : function(col, row, massUpdate)
{
if (col != this.__focusedCol || row != this.__focusedRow)
{
var oldRow = this.__focusedRow;
this.__focusedCol = col;
this.__focusedRow = row;
// Update the focused row background
if (row != oldRow && !massUpdate)
{
if (oldRow !== null) {
this.updateContent(false, null, oldRow, true);
}
if (row !== null) {
this.updateContent(false, null, row, true);
}
}
}
},
/**
* Event handler. Called when the selection has changed.
*/
onSelectionChanged : function() {
this.updateContent(false, null, null, true);
},
/**
* Event handler. Called when the table gets or looses the focus.
*/
onFocusChanged : function() {
this.updateContent(false, null, null, true);
},
/**
* Sets the column width.
*
* @param col {Integer} the column to change the width for.
* @param width {Integer} the new width.
*/
setColumnWidth : function(col, width) {
this.updateContent(true);
},
/**
* Event handler. Called the column order has changed.
*
*/
onColOrderChanged : function() {
this.updateContent(true);
},
/**
* Event handler. Called when the pane model has changed.
*/
onPaneModelChanged : function() {
this.updateContent(true);
},
/**
* Event handler. Called when the table model data has changed.
*
* @param firstRow {Integer} The index of the first row that has changed.
* @param lastRow {Integer} The index of the last row that has changed.
* @param firstColumn {Integer} The model index of the first column that has changed.
* @param lastColumn {Integer} The model index of the last column that has changed.
*/
onTableModelDataChanged : function(firstRow, lastRow, firstColumn, lastColumn)
{
this.__rowCacheClear();
var paneFirstRow = this.getFirstVisibleRow();
var rowCount = this.getVisibleRowCount();
if (lastRow == -1 || lastRow >= paneFirstRow && firstRow < paneFirstRow + rowCount)
{
// The change intersects this pane
this.updateContent();
}
},
/**
* Event handler. Called when the table model meta data has changed.
*
*/
onTableModelMetaDataChanged : function() {
this.updateContent(true);
},
// property apply method
_applyMaxCacheLines : function(value, old)
{
if (this.__rowCacheCount >= value && value !== -1) {
this.__rowCacheClear();
}
},
/**
* Clear the row cache
*/
__rowCacheClear : function()
{
this.__rowCache = [];
this.__rowCacheCount = 0;
},
/**
* Get a line from the row cache.
*
* @param row {Integer} Row index to get
* @param selected {Boolean} Whether the row is currently selected
* @param focused {Boolean} Whether the row is currently focused
* @return {String|null} The cached row or null if a row with the given
* index is not cached.
*/
__rowCacheGet : function(row, selected, focused)
{
if (!selected && !focused && this.__rowCache[row]) {
return this.__rowCache[row];
} else {
return null;
}
},
/**
* Add a line to the row cache.
*
* @param row {Integer} Row index to set
* @param rowString {String} computed row string to cache
* @param selected {Boolean} Whether the row is currently selected
* @param focused {Boolean} Whether the row is currently focused
*/
__rowCacheSet : function(row, rowString, selected, focused)
{
var maxCacheLines = this.getMaxCacheLines();
if (
!selected &&
!focused &&
!this.__rowCache[row] &&
maxCacheLines > 0
) {
this._applyMaxCacheLines(maxCacheLines);
this.__rowCache[row] = rowString;
this.__rowCacheCount += 1;
}
},
/**
* Updates the content of the pane.
*
* @param completeUpdate {Boolean ? false} if true a complete update is performed.
* On a complete update all cell widgets are recreated.
* @param scrollOffset {Integer ? null} If set specifies how many rows to scroll.
* @param onlyRow {Integer ? null} if set only the specified row will be updated.
* @param onlySelectionOrFocusChanged {Boolean ? false} if true, cell values won't
* be updated. Only the row background will.
*/
updateContent : function(completeUpdate, scrollOffset, onlyRow, onlySelectionOrFocusChanged)
{
if (completeUpdate) {
this.__rowCacheClear();
}
if (scrollOffset && Math.abs(scrollOffset) <= Math.min(10, this.getVisibleRowCount())) {
this._scrollContent(scrollOffset);
} else if (onlySelectionOrFocusChanged && !this.getTable().getAlwaysUpdateCells()) {
this._updateRowStyles(onlyRow);
} else {
this._updateAllRows();
}
},
/**
* If only focus or selection changes it is sufficient to only update the
* row styles. This method updates the row styles of all visible rows or
* of just one row.
*
* @param onlyRow {Integer|null ? null} If this parameter is set only the row
* with this index is updated.
*/
_updateRowStyles : function(onlyRow)
{
var elem = this.getContentElement().getDomElement();
if (!elem || !elem.firstChild) {
this._updateAllRows();
return;
}
var table = this.getTable();
var selectionModel = table.getSelectionModel();
var tableModel = table.getTableModel();
var rowRenderer = table.getDataRowRenderer();
var rowNodes = elem.firstChild.childNodes;
var cellInfo = { table : table };
// We don't want to execute the row loop below more than necessary. If
// onlyRow is not null, we want to do the loop only for that row.
// In that case, we start at (set the "row" variable to) that row, and
// stop at (set the "end" variable to the offset of) the next row.
var row = this.getFirstVisibleRow();
var y = 0;
// How many rows do we need to update?
var end = rowNodes.length;
if (onlyRow != null)
{
// How many rows are we skipping?
var offset = onlyRow - row;
if (offset >= 0 && offset < end)
{
row = onlyRow;
y = offset;
end = offset + 1;
} else
{
return;
}
}
for (; y<end; y++, row++)
{
cellInfo.row = row;
cellInfo.selected = selectionModel.isSelectedIndex(row);
cellInfo.focusedRow = (this.__focusedRow == row);
cellInfo.rowData = tableModel.getRowData(row);
rowRenderer.updateDataRowElement(cellInfo, rowNodes[y]);
};
},
/**
* Get the HTML table fragment for the given row range.
*
* @param firstRow {Integer} Index of the first row
* @param rowCount {Integer} Number of rows
* @return {String} The HTML table fragment for the given row range.
*/
_getRowsHtml : function(firstRow, rowCount)
{
var table = this.getTable();
var selectionModel = table.getSelectionModel();
var tableModel = table.getTableModel();
var columnModel = table.getTableColumnModel();
var paneModel = this.getPaneScroller().getTablePaneModel();
var rowRenderer = table.getDataRowRenderer();
tableModel.prefetchRows(firstRow, firstRow + rowCount - 1);
var rowHeight = table.getRowHeight();
var colCount = paneModel.getColumnCount();
var left = 0;
var cols = [];
// precompute column properties
for (var x=0; x<colCount; x++)
{
var col = paneModel.getColumnAtX(x);
var cellWidth = columnModel.getColumnWidth(col);
cols.push({
col: col,
xPos: x,
editable: tableModel.isColumnEditable(col),
focusedCol: this.__focusedCol == col,
styleLeft: left,
styleWidth: cellWidth
});
left += cellWidth;
}
var rowsArr = [];
var paneReloadsData = false;
for (var row=firstRow; row < firstRow + rowCount; row++)
{
var selected = selectionModel.isSelectedIndex(row);
var focusedRow = (this.__focusedRow == row);
var cachedRow = this.__rowCacheGet(row, selected, focusedRow);
if (cachedRow) {
rowsArr.push(cachedRow);
continue;
}
var rowHtml = [];
var cellInfo = { table : table };
cellInfo.styleHeight = rowHeight;
cellInfo.row = row;
cellInfo.selected = selected;
cellInfo.focusedRow = focusedRow;
cellInfo.rowData = tableModel.getRowData(row);
if (!cellInfo.rowData) {
paneReloadsData = true;
}
rowHtml.push('<div ');
var rowAttributes = rowRenderer.getRowAttributes(cellInfo);
if (rowAttributes) {
rowHtml.push(rowAttributes);
}
var rowClass = rowRenderer.getRowClass(cellInfo);
if (rowClass) {
rowHtml.push('class="', rowClass, '" ');
}
var rowStyle = rowRenderer.createRowStyle(cellInfo);
rowStyle += ";position:relative;" + rowRenderer.getRowHeightStyle(rowHeight)+ "width:100%;";
if (rowStyle) {
rowHtml.push('style="', rowStyle, '" ');
}
rowHtml.push('>');
var stopLoop = false;
for (x=0; x<colCount && !stopLoop; x++)
{
var col_def = cols[x];
for (var attr in col_def) {
cellInfo[attr] = col_def[attr];
}
var col = cellInfo.col;
// Use the "getValue" method of the tableModel to get the cell's
// value working directly on the "rowData" object
// (-> cellInfo.rowData[col];) is not a solution because you can't
// work with the columnIndex -> you have to use the columnId of the
// columnIndex This is exactly what the method "getValue" does
cellInfo.value = tableModel.getValue(col, row);
var cellRenderer = columnModel.getDataCellRenderer(col);
// Retrieve the current default cell style for this column.
cellInfo.style = cellRenderer.getDefaultCellStyle();
// Allow a cell renderer to tell us not to draw any further cells in
// the row. Older, or traditional cell renderers don't return a
// value, however, from createDataCellHtml, so assume those are
// returning false.
//
// Tested with http://tinyurl.com/333hyhv
stopLoop =
cellRenderer.createDataCellHtml(cellInfo, rowHtml) || false;
}
rowHtml.push('</div>');
var rowString = rowHtml.join("");
this.__rowCacheSet(row, rowString, selected, focusedRow);
rowsArr.push(rowString);
}
this.fireDataEvent("paneReloadsData", paneReloadsData);
return rowsArr.join("");
},
/**
* Scrolls the pane's contents by the given offset.
*
* @param rowOffset {Integer} Number of lines to scroll. Scrolling up is
* represented by a negative offset.
*/
_scrollContent : function(rowOffset)
{
var el = this.getContentElement().getDomElement();
if (!(el && el.firstChild)) {
this._updateAllRows();
return;
}
var tableBody = el.firstChild;
var tableChildNodes = tableBody.childNodes;
var rowCount = this.getVisibleRowCount();
var firstRow = this.getFirstVisibleRow();
var tabelModel = this.getTable().getTableModel();
var modelRowCount = 0;
modelRowCount = tabelModel.getRowCount();
// don't handle this special case here
if (firstRow + rowCount > modelRowCount) {
this._updateAllRows();
return;
}
// remove old lines
var removeRowBase = rowOffset < 0 ? rowCount + rowOffset : 0;
var addRowBase = rowOffset < 0 ? 0: rowCount - rowOffset;
for (var i=Math.abs(rowOffset)-1; i>=0; i--)
{
var rowElem = tableChildNodes[removeRowBase];
try {
tableBody.removeChild(rowElem);
} catch(exp) {
break;
}
}
// render new lines
if (!this.__tableContainer) {
this.__tableContainer = document.createElement("div");
}
var tableDummy = '<div>';
tableDummy += this._getRowsHtml(firstRow + addRowBase, Math.abs(rowOffset));
tableDummy += '</div>';
this.__tableContainer.innerHTML = tableDummy;
var newTableRows = this.__tableContainer.firstChild.childNodes;
// append new lines
if (rowOffset > 0)
{
for (var i=newTableRows.length-1; i>=0; i--)
{
var rowElem = newTableRows[0];
tableBody.appendChild(rowElem);
}
}
else
{
for (var i=newTableRows.length-1; i>=0; i--)
{
var rowElem = newTableRows[newTableRows.length-1];
tableBody.insertBefore(rowElem, tableBody.firstChild);
}
}
// update focus indicator
if (this.__focusedRow !== null)
{
this._updateRowStyles(this.__focusedRow - rowOffset);
this._updateRowStyles(this.__focusedRow);
}
this.fireEvent("paneUpdated");
},
/**
* Updates the content of the pane (implemented using array joins).
*/
_updateAllRows : function()
{
var elem = this.getContentElement().getDomElement();
if (!elem) {
// pane has not yet been rendered
this.addListenerOnce("appear", this._updateAllRows, this);
return;
}
var table = this.getTable();
var tableModel = table.getTableModel();
var paneModel = this.getPaneScroller().getTablePaneModel();
var colCount = paneModel.getColumnCount();
var rowHeight = table.getRowHeight();
var firstRow = this.getFirstVisibleRow();
var rowCount = this.getVisibleRowCount();
var modelRowCount = tableModel.getRowCount();
if (firstRow + rowCount > modelRowCount) {
rowCount = Math.max(0, modelRowCount - firstRow);
}
var rowWidth = paneModel.getTotalWidth();
var htmlArr;
// If there are any rows...
if (rowCount > 0)
{
// ... then create a div for them and add the rows to it.
htmlArr =
[
"<div style='",
"width: 100%;",
(table.getForceLineHeight()
? "line-height: " + rowHeight + "px;"
: ""),
"overflow: hidden;",
"'>",
this._getRowsHtml(firstRow, rowCount),
"</div>"
];
}
else
{
// Otherwise, don't create the div, as even an empty div creates a
// white row in IE.
htmlArr = [];
}
var data = htmlArr.join("");
elem.innerHTML = data;
this.setWidth(rowWidth);
this.__lastColCount = colCount;
this.__lastRowCount = rowCount;
this.fireEvent("paneUpdated");
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function() {
this.__tableContainer = this.__paneScroller = this.__rowCache = null;
this.removeListener("track", this._onTrack, this);
}
});