UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

450 lines (377 loc) 12.4 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2009 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Fabian Jakobs (fjakobs) ************************************************************************ */ /** * EXPERIMENTAL! * * The CellSpanManager manages cells, which span several rows or columns. * * It provides functionality to compute, which spanning cells are visible * in a given view port and how they have to be placed. */ qx.Class.define("qx.ui.virtual.layer.CellSpanManager", { extend : qx.core.Object, /** * @param rowConfig {qx.ui.virtual.core.Axis} The row configuration of the pane * in which the cells will be rendered * @param columnConfig {qx.ui.virtual.core.Axis} The column configuration of the pane * in which the cells will be rendered */ construct : function(rowConfig, columnConfig) { this.base(arguments); if (qx.core.Environment.get("qx.debug")) { this.assertInstance(rowConfig, qx.ui.virtual.core.Axis); this.assertInstance(columnConfig, qx.ui.virtual.core.Axis); } this._cells = {}; this._invalidateSortCache(); this._invalidatePositionCache(); rowConfig.addListener("change", this._onRowConfigChange, this); columnConfig.addListener("change", this._onColumnConfigChange, this); this._rowConfig = rowConfig; this._columnConfig = columnConfig; }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { /** * Add a spanning cell to the manager. * * @param id {String} Unique id for the cell definition. This id is required * for removing the cell from the manager * @param row {PositiveInteger} The cell's row * @param column {PositiveInteger} The cell's column * @param rowSpan {PositiveInteger} The number of rows the cells spans * @param columnSpan {PositiveInteger} The number of columns the cells spans */ addCell : function(id, row, column, rowSpan, columnSpan) { this._cells[id] = { firstRow: row, lastRow : row + rowSpan - 1, firstColumn: column, lastColumn: column + columnSpan - 1, id: id }; this._invalidateSortCache(); }, /** * Remove a cell from the manager * * @param id {String} The id of the cell to remove */ removeCell : function(id) { delete(this._cells[id]); this._invalidateSortCache(); }, /** * Invalidate the sort cache */ _invalidateSortCache : function() { this._sorted = {}; }, /** * Get the cell array sorted by the given key (ascending) * * @param key {String} The sort key. One of <code>firstRow</code>, * <code>lastRow</code>, <code>firstColumn</code> or <code>lastColumn</code> * @return {Map[]} sorted array of cell descriptions */ _getSortedCells : function(key) { if (this._sorted[key]) { return this._sorted[key]; } var sorted = this._sorted[key] = Object.values(this._cells); sorted.sort(function(a, b) { return a[key] < b[key] ? -1 : 1; }); return sorted; }, /** * Finds all cells with a sort key within the given range. * * Complexity: O(log n) * * @param key {String} The key to search for * @param min {Integer} minimum value * @param max {Integer} maximum value (inclusive) * @return {Map} Map, which will contain the search results */ _findCellsInRange : function(key, min, max) { var cells = this._getSortedCells(key); if (cells.length == 0) { return {}; } var start = 0; var end = cells.length-1; // find first cell, which is >= "min" while (true) { var pivot = start + ((end - start) >> 1); var cell = cells[pivot]; if ( cell[key] >= min && (pivot == 0 || cells[pivot-1][key] < min) ) { // the start cell was found break; } if (cell[key] >= min) { end = pivot - 1; } else { start = pivot + 1; } if (start > end) { // nothing found return {}; } } var result = {}; var cell = cells[pivot]; while (cell && cell[key] >= min && cell[key] <= max) { result[cell.id] = cell; cell = cells[pivot++]; } return result; }, /** * Find all cells, which are visible in the given grid window. * * @param firstRow {PositiveInteger} first visible row * @param firstColumn {PositiveInteger} first visible column * @param rowCount {PositiveInteger} number of rows in the window * @param columnCount {PositiveInteger} number of columns in the window * @return {Map[]} The array of found cell descriptions. A cell description * contains the keys <code>firstRow</code>, <code>lastRow</code>, * <code>firstColumn</code> or <code>lastColumn</code> */ findCellsInWindow : function(firstRow, firstColumn, rowCount, columnCount) { var verticalInWindow = {}; if (rowCount > 0) { var lastRow = firstRow + rowCount - 1; qx.lang.Object.mergeWith( verticalInWindow, this._findCellsInRange("firstRow", firstRow, lastRow) ); qx.lang.Object.mergeWith( verticalInWindow, this._findCellsInRange("lastRow", firstRow, lastRow) ); } var horizontalInWindow = {}; if (columnCount > 0) { var lastColumn = firstColumn + columnCount - 1; qx.lang.Object.mergeWith( horizontalInWindow, this._findCellsInRange("firstColumn", firstColumn, lastColumn) ); qx.lang.Object.mergeWith( horizontalInWindow, this._findCellsInRange("lastColumn", firstColumn, lastColumn) ); } return this.__intersectionAsArray(horizontalInWindow, verticalInWindow); }, /** * Return the intersection of two maps as an array. The objects intersect if * they have the same keys. * * @param setA {Object} The first map * @param setB {Object} The second map * @return {String[]} An array keys found in both maps */ __intersectionAsArray : function(setA, setB) { var intersection = []; for (var key in setA) { if (setB[key]) { intersection.push(setB[key]); } } return intersection; }, /** * Event handler for row configuration changes * * @param e {qx.event.type.Event} the event object */ _onRowConfigChange : function(e) { this._rowPos = []; }, /** * Event handler for column configuration changes * * @param e {qx.event.type.Event} the event object */ _onColumnConfigChange : function(e) { this._columnPos = []; }, /** * Invalidates the row/column position cache */ _invalidatePositionCache : function() { this._rowPos = []; this._columnPos = []; }, /** * Get the pixel start position of the given row * * @param row {Integer} The row index * @return {Integer} The pixel start position of the given row */ _getRowPosition : function(row) { var pos = this._rowPos[row]; if (pos !== undefined) { return pos; } pos = this._rowPos[row] = this._rowConfig.getItemPosition(row); return pos; }, /** * Get the pixel start position of the given column * * @param column {Integer} The column index * @return {Integer} The pixel start position of the given column */ _getColumnPosition : function(column) { var pos = this._columnPos[column]; if (pos !== undefined) { return pos; } pos = this._columnPos[column] = this._columnConfig.getItemPosition(column); return pos; }, /** * Get the bounds of a single cell * * @param cell {Map} the cell description as returned by * {@link #findCellsInWindow} to get the bounds for * @param firstVisibleRow {Map} The pane's first visible row * @param firstVisibleColumn {Map} The pane's first visible column * @return {Map} Boundaries map with the keys <code>left</code>, * <code>top</code>, <code>width</code> and <code>height</code> */ _getSingleCellBounds : function(cell, firstVisibleRow, firstVisibleColumn) { var bounds = { left: 0, top: 0, width: 0, height: 0 }; bounds.height = this._getRowPosition(cell.lastRow) + this._rowConfig.getItemSize(cell.lastRow) - this._getRowPosition(cell.firstRow); bounds.top = this._getRowPosition(cell.firstRow) - this._getRowPosition(firstVisibleRow); bounds.width = this._getColumnPosition(cell.lastColumn) + this._columnConfig.getItemSize(cell.lastColumn) - this._getColumnPosition(cell.firstColumn); bounds.left = this._getColumnPosition(cell.firstColumn) - this._getColumnPosition(firstVisibleColumn); return bounds; }, /** * Get the bounds of a list of cells as returned by {@link #findCellsInWindow} * * @param cells {Map[]} Array of cell descriptions * @param firstVisibleRow {Map} The pane's first visible row * @param firstVisibleColumn {Map} The pane's first visible column * @return {Map[]} Array, which contains a bounds map for each cell. */ getCellBounds : function(cells, firstVisibleRow, firstVisibleColumn) { var bounds = []; for (var i=0, l=cells.length; i<l; i++) { bounds.push(this._getSingleCellBounds( cells[i], firstVisibleRow, firstVisibleColumn) ); } return bounds; }, /** * Compute a bitmap, which marks for each visible cell, whether the cell * is covered by a spanning cell. * * @param cells {Map[]} Array of cell descriptions as returned by * {@link #findCellsInWindow}. * @param firstRow {PositiveInteger} first visible row * @param firstColumn {PositiveInteger} first visible column * @param rowCount {PositiveInteger} number of rows in the window * @param columnCount {PositiveInteger} number of columns in the window * @return {Map[][]} Two dimensional array, which contains a <code>1</code> * for each visible cell, which is covered by a spanned cell. */ computeCellSpanMap : function(cells, firstRow, firstColumn, rowCount, columnCount) { var map = []; if (rowCount <= 0) { return map; } var lastRow = firstRow + rowCount - 1; for (var i=firstRow; i<= lastRow; i++) { map[i] = []; } if (columnCount <= 0) { return map; } var lastColumn = firstColumn + columnCount - 1; for (var i=0, l=cells.length; i<l; i++) { var cell = cells[i]; var rowStartIndex = Math.max(firstRow, cell.firstRow); var rowEndIndex = Math.min(lastRow, cell.lastRow); var row; for (var rowIndex=rowStartIndex; rowIndex <= rowEndIndex; rowIndex++) { row = map[rowIndex]; var columnStartIndex = Math.max(firstColumn, cell.firstColumn); var columnEndIndex = Math.min(lastColumn, cell.lastColumn); for (var columnIndex=columnStartIndex; columnIndex <= columnEndIndex; columnIndex++) { row[columnIndex] = 1; } } } return map; } }, destruct : function() { this._rowConfig.removeListener("change", this._onRowConfigChange, this); this._columnConfig.removeListener("change", this._onColumnConfigChange, this); this._cells = this._sorted = this._rowPos = this._columnPos = this._rowConfig = this._columnConfig = null; } });