UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

739 lines (602 loc) 20.4 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) * 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); } });