UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

981 lines (820 loc) 26.7 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) * Jonathan Weiß (jonathan_rass) ************************************************************************ */ /** * EXPERIMENTAL! * * The Pane provides a window of a larger virtual grid. * * The actual rendering is performed by one or several layers ({@link ILayer}. * The pane computes, which cells of the virtual area is visible and instructs * the layers to render these cells. */ qx.Class.define("qx.ui.virtual.core.Pane", { extend : qx.ui.core.Widget, /** * @param rowCount {Integer?0} The number of rows of the virtual grid. * @param columnCount {Integer?0} The number of columns of the virtual grid. * @param cellHeight {Integer?10} The default cell height. * @param cellWidth {Integer?10} The default cell width. */ construct : function(rowCount, columnCount, cellHeight, cellWidth) { this.base(arguments); this.__rowConfig = new qx.ui.virtual.core.Axis(cellHeight, rowCount); this.__columnConfig = new qx.ui.virtual.core.Axis(cellWidth, columnCount); this.__scrollTop = 0; this.__scrollLeft = 0; this.__paneHeight = 0; this.__paneWidth = 0; this.__layerWindow = {}; this.__jobs = {}; // create layer container. The container does not have a layout manager // layers are positioned using "setUserBounds" this.__layerContainer = new qx.ui.container.Composite(); this.__layerContainer.setUserBounds(0, 0, 0, 0); this._add(this.__layerContainer); this.__layers = []; this.__rowConfig.addListener("change", this.fullUpdate, this); this.__columnConfig.addListener("change", this.fullUpdate, this); this.addListener("resize", this._onResize, this); this.addListenerOnce("appear", this._onAppear, this); this.addListener("pointerdown", this._onPointerDown, this); this.addListener("tap", this._onTap, this); this.addListener("dbltap", this._onDbltap, this); this.addListener("contextmenu", this._onContextmenu, this); }, events : { /** Fired if a cell is tapped. */ cellTap : "qx.ui.virtual.core.CellEvent", /** Fired if a cell is right-clicked. */ cellContextmenu : "qx.ui.virtual.core.CellEvent", /** Fired if a cell is double-tapped. */ cellDbltap : "qx.ui.virtual.core.CellEvent", /** Fired on resize of either the container or the (virtual) content. */ update : "qx.event.type.Event", /** Fired if the pane is scrolled horizontally. */ scrollX : "qx.event.type.Data", /** Fired if the pane is scrolled vertically. */ scrollY : "qx.event.type.Data" }, properties : { // overridden width : { refine : true, init : 400 }, // overridden height : { refine : true, init : 300 } }, members : { __rowConfig : null, __columnConfig : null, __scrollTop : null, __scrollLeft : null, __paneHeight : null, __paneWidth : null, __layerWindow : null, __jobs : null, __layerContainer : null, __layers : null, __dontFireUpdate : null, __columnSizes : null, __rowSizes : null, __pointerDownCoords : null, /* --------------------------------------------------------------------------- ACCESSOR METHODS --------------------------------------------------------------------------- */ /** * Get the axis object, which defines the row numbers and the row sizes. * * @return {qx.ui.virtual.core.Axis} The row configuration. */ getRowConfig : function() { return this.__rowConfig; }, /** * Get the axis object, which defines the column numbers and the column sizes. * * @return {qx.ui.virtual.core.Axis} The column configuration. */ getColumnConfig : function() { return this.__columnConfig; }, /* --------------------------------------------------------------------------- LAYER MANAGEMENT --------------------------------------------------------------------------- */ /** * Returns an array containing the layer container. * * @return {Object[]} The layer container array. */ getChildren : function() { return [this.__layerContainer]; }, /** * Add a layer to the layer container. * * @param layer {qx.ui.virtual.core.ILayer} The layer to add. */ addLayer : function(layer) { if (qx.core.Environment.get("qx.debug")) { this.assertInterface(layer, qx.ui.virtual.core.ILayer); } this.__layers.push(layer); layer.setUserBounds(0, 0, 0, 0); this.__layerContainer.add(layer); }, /** * Get a list of all layers. * * @return {qx.ui.virtual.core.ILayer[]} List of the pane's layers. */ getLayers : function() { return this.__layers; }, /** * Get a list of all visible layers. * * @return {qx.ui.virtual.core.ILayer[]} List of the pane's visible layers. */ getVisibleLayers : function() { var layers = []; for (var i=0; i<this.__layers.length; i++) { var layer = this.__layers[i]; if (layer.isVisible()) { layers.push(layer); } } return layers; }, /* --------------------------------------------------------------------------- SCROLL SUPPORT --------------------------------------------------------------------------- */ /** * The maximum horizontal scroll position. * * @return {Integer} Maximum horizontal scroll position. */ getScrollMaxX : function() { var paneSize = this.getInnerSize(); if (paneSize) { return Math.max(0, this.__columnConfig.getTotalSize() - paneSize.width); } return 0; }, /** * The maximum vertical scroll position. * * @return {Integer} Maximum vertical scroll position. */ getScrollMaxY : function() { var paneSize = this.getInnerSize(); if (paneSize) { return Math.max(0, this.__rowConfig.getTotalSize() - paneSize.height); } return 0; }, /** * Scrolls the content to the given left coordinate. * * @param value {Integer} The vertical position to scroll to. */ setScrollY : function(value) { var max = this.getScrollMaxY(); if (value < 0) { value = 0; } else if (value > max) { value = max; } if (this.__scrollTop !== value) { var old = this.__scrollTop; this.__scrollTop = value; this._deferredUpdateScrollPosition(); this.fireDataEvent("scrollY", value, old); } }, /** * Returns the vertical scroll offset. * * @return {Integer} The vertical scroll offset. */ getScrollY : function() { return this.__scrollTop; }, /** * Scrolls the content to the given top coordinate. * * @param value {Integer} The horizontal position to scroll to. */ setScrollX : function(value) { var max = this.getScrollMaxX(); if (value < 0) { value = 0; } else if (value > max) { value = max; } if (value !== this.__scrollLeft) { var old = this.__scrollLeft; this.__scrollLeft = value; this._deferredUpdateScrollPosition(); this.fireDataEvent("scrollX", value, old); } }, /** * Returns the horizontal scroll offset. * * @return {Integer} The horizontal scroll offset. */ getScrollX : function() { return this.__scrollLeft; }, /** * The (virtual) size of the content. * * @return {Map} Size of the content (keys: <code>width</code> and * <code>height</code>). */ getScrollSize : function() { return { width: this.__columnConfig.getTotalSize(), height: this.__rowConfig.getTotalSize() }; }, /* --------------------------------------------------------------------------- SCROLL INTO VIEW SUPPORT --------------------------------------------------------------------------- */ /** * Scrolls a row into the visible area of the pane. * * @param row {Integer} The row's index. */ scrollRowIntoView : function(row) { var bounds = this.getBounds(); if (!bounds) { this.addListenerOnce("appear", function() { // It's important that the registered events are first dispatched. qx.event.Timer.once(function() { this.scrollRowIntoView(row); }, this, 0); }, this); return; } var itemTop = this.__rowConfig.getItemPosition(row); var itemBottom = itemTop + this.__rowConfig.getItemSize(row); var scrollTop = this.getScrollY(); if (itemTop < scrollTop) { this.setScrollY(itemTop); } else if (itemBottom > scrollTop + bounds.height) { this.setScrollY(itemBottom - bounds.height); } }, /** * Scrolls a column into the visible area of the pane. * * @param column {Integer} The column's index. */ scrollColumnIntoView : function(column) { var bounds = this.getBounds(); if (!bounds) { this.addListenerOnce("appear", function() { // It's important that the registered events are first dispatched. qx.event.Timer.once(function() { this.scrollColumnIntoView(column); }, this, 0); }, this); return; } var itemLeft = this.__columnConfig.getItemPosition(column); var itemRight = itemLeft + this.__columnConfig.getItemSize(column); var scrollLeft = this.getScrollX(); if (itemLeft < scrollLeft) { this.setScrollX(itemLeft); } else if (itemRight > scrollLeft + bounds.width) { this.setScrollX(itemRight - bounds.width); } }, /** * Scrolls a grid cell into the visible area of the pane. * * @param row {Integer} The cell's row index. * @param column {Integer} The cell's column index. */ scrollCellIntoView : function(column, row) { var bounds = this.getBounds(); if (!bounds) { this.addListenerOnce("appear", function() { // It's important that the registered events are first dispatched. qx.event.Timer.once(function() { this.scrollCellIntoView(column, row); }, this, 0); }, this); return; } this.scrollColumnIntoView(column); this.scrollRowIntoView(row); }, /* --------------------------------------------------------------------------- CELL SUPPORT --------------------------------------------------------------------------- */ /** * Get the grid cell at the given absolute document coordinates. This method * can be used to convert the pointer position returned by * {@link qx.event.type.Pointer#getDocumentLeft} and * {@link qx.event.type.Pointer#getDocumentLeft} into cell coordinates. * * @param documentX {Integer} The x coordinate relative to the viewport * origin. * @param documentY {Integer} The y coordinate relative to the viewport * origin. * @return {Map|null} A map containing the <code>row</code> and <code>column</code> * of the found cell. If the coordinate is outside of the pane's bounds * or there is no cell at the coordinate <code>null</code> is returned. */ getCellAtPosition: function(documentX, documentY) { var rowData, columnData; var paneLocation = this.getContentLocation(); if ( !paneLocation || documentY < paneLocation.top || documentY >= paneLocation.bottom || documentX < paneLocation.left || documentX >= paneLocation.right ) { return null; } rowData = this.__rowConfig.getItemAtPosition( this.getScrollY() + documentY - paneLocation.top ); columnData = this.__columnConfig.getItemAtPosition( this.getScrollX() + documentX - paneLocation.left ); if (!rowData || !columnData) { return null; } return { row : rowData.index, column : columnData.index }; }, /* --------------------------------------------------------------------------- PREFETCH SUPPORT --------------------------------------------------------------------------- */ /** * Increase the layers width beyond the needed width to improve * horizontal scrolling. The layers are only resized if invisible parts * left/right of the pane window are smaller than minLeft/minRight. * * @param minLeft {Integer} Only prefetch if the invisible part left of the * pane window if smaller than this (pixel) value. * @param maxLeft {Integer} The amount of pixel the layers should reach * left of the pane window. * @param minRight {Integer} Only prefetch if the invisible part right of the * pane window if smaller than this (pixel) value. * @param maxRight {Integer} The amount of pixel the layers should reach * right of the pane window. */ prefetchX : function(minLeft, maxLeft, minRight, maxRight) { var layers = this.getVisibleLayers(); if (layers.length == 0) { return; } var bounds = this.getBounds(); if (!bounds) { return; } var paneRight = this.__scrollLeft + bounds.width; var rightAvailable = this.__paneWidth - paneRight; if ( this.__scrollLeft - this.__layerWindow.left < Math.min(this.__scrollLeft, minLeft) || this.__layerWindow.right - paneRight < Math.min(rightAvailable, minRight) ) { var left = Math.min(this.__scrollLeft, maxLeft); var right = Math.min(rightAvailable, maxRight); this._setLayerWindow( layers, this.__scrollLeft - left, this.__scrollTop, bounds.width + left + right, bounds.height, false ); } }, /** * Increase the layers height beyond the needed height to improve * vertical scrolling. The layers are only resized if invisible parts * above/below the pane window are smaller than minAbove/minBelow. * * @param minAbove {Integer} Only prefetch if the invisible part above the * pane window if smaller than this (pixel) value. * @param maxAbove {Integer} The amount of pixel the layers should reach * above the pane window. * @param minBelow {Integer} Only prefetch if the invisible part below the * pane window if smaller than this (pixel) value. * @param maxBelow {Integer} The amount of pixel the layers should reach * below the pane window. */ prefetchY : function(minAbove, maxAbove, minBelow, maxBelow) { var layers = this.getVisibleLayers(); if (layers.length == 0) { return; } var bounds = this.getBounds(); if (!bounds) { return; } var paneBottom = this.__scrollTop + bounds.height; var belowAvailable = this.__paneHeight - paneBottom; if ( this.__scrollTop - this.__layerWindow.top < Math.min(this.__scrollTop, minAbove) || this.__layerWindow.bottom - paneBottom < Math.min(belowAvailable, minBelow) ) { var above = Math.min(this.__scrollTop, maxAbove); var below = Math.min(belowAvailable, maxBelow); this._setLayerWindow( layers, this.__scrollLeft, this.__scrollTop - above, bounds.width, bounds.height + above + below, false ); } }, /* --------------------------------------------------------------------------- EVENT LISTENER --------------------------------------------------------------------------- */ /** * Resize event handler. * * Updates the visible window. */ _onResize : function() { if (this.getContentElement().getDomElement()) { this.__dontFireUpdate = true; this._updateScrollPosition(); this.__dontFireUpdate = null; this.fireEvent("update"); } }, /** * Resize event handler. Do a full update on first appear. */ _onAppear : function() { this.fullUpdate(); }, /** * Event listener for pointer down. Remembers cell position to prevent pointer event when cell position change. * * @param e {qx.event.type.Pointer} The incoming pointer event. */ _onPointerDown : function(e) { this.__pointerDownCoords = this.getCellAtPosition(e.getDocumentLeft(), e.getDocumentTop()); }, /** * Event listener for pointer taps. Fires an cellTap event. * * @param e {qx.event.type.Pointer} The incoming pointer event. */ _onTap : function(e) { this.__handlePointerCellEvent(e, "cellTap"); }, /** * Event listener for context menu taps. Fires an cellContextmenu event. * * @param e {qx.event.type.Pointer} The incoming pointer event. */ _onContextmenu : function(e) { this.__handlePointerCellEvent(e, "cellContextmenu"); }, /** * Event listener for double taps. Fires an cellDbltap event. * * @param e {qx.event.type.Pointer} The incoming pointer event. */ _onDbltap : function(e) { this.__handlePointerCellEvent(e, "cellDbltap"); }, /** * Fixed scrollbar position whenever it is out of range * it can happen when removing an item from the list reducing * the max value for scrollY #8976 */ _checkScrollBounds: function() { var maxx = this.getScrollMaxX(); var maxy = this.getScrollMaxY(); if (this.__scrollLeft < 0) { this.__scrollLeft = 0; } else if (this.__scrollLeft > maxx) { this.__scrollLeft = maxx; } if (this.__scrollTop < 0) { this.__scrollTop = 0; } else if (this.__scrollTop > maxy) { this.__scrollTop = maxy; } }, /** * Converts a pointer event into a cell event and fires the cell event if the * pointer is over a cell. * * @param e {qx.event.type.Pointer} The pointer event. * @param cellEventType {String} The name of the cell event to fire. */ __handlePointerCellEvent : function(e, cellEventType) { var coords = this.getCellAtPosition(e.getDocumentLeft(), e.getDocumentTop()); if (!coords) { return; } var pointerDownCoords = this.__pointerDownCoords; if (pointerDownCoords == null || pointerDownCoords.row !== coords.row || pointerDownCoords.column !== coords.column) { return; } this.fireNonBubblingEvent( cellEventType, qx.ui.virtual.core.CellEvent, [this, e, coords.row, coords.column] ); }, /* --------------------------------------------------------------------------- PANE UPDATE --------------------------------------------------------------------------- */ // overridden syncWidget : function(jobs) { if (this.__jobs._fullUpdate) { this._checkScrollBounds(); this._fullUpdate(); } else if (this.__jobs._updateScrollPosition) { this._checkScrollBounds(); this._updateScrollPosition(); } this.__jobs = {}; }, /** * Sets the size of the layers to contain the cells at the pixel position * "left/right" up to "left+minHeight/right+minHeight". The offset of the * layer container is adjusted to respect the pane's scroll top and scroll * left values. * * @param layers {qx.ui.virtual.core.ILayer[]} List of layers to update. * @param left {Integer} Maximum left pixel coordinate of the layers. * @param top {Integer} Maximum top pixel coordinate of the layers. * @param minWidth {Integer} The minimum end coordinate of the layers will * be larger than <code>left+minWidth</code>. * @param minHeight {Integer} The minimum end coordinate of the layers will * be larger than <code>top+minHeight</code>. * @param doFullUpdate {Boolean?false} Whether a full update on the layer * should be performed of if only the layer window should be updated. */ _setLayerWindow : function(layers, left, top, minWidth, minHeight, doFullUpdate) { var rowCellData = this.__rowConfig.getItemAtPosition(top); if (rowCellData) { var firstRow = rowCellData.index; var rowSizes = this.__rowConfig.getItemSizes(firstRow, minHeight + rowCellData.offset); var layerHeight = qx.lang.Array.sum(rowSizes); var layerTop = top - rowCellData.offset; var layerBottom = top - rowCellData.offset + layerHeight; } else { var firstRow = 0; var rowSizes = []; var layerHeight = 0; var layerTop = 0; var layerBottom = 0; } var columnCellData = this.__columnConfig.getItemAtPosition(left); if (columnCellData) { var firstColumn = columnCellData.index; var columnSizes = this.__columnConfig.getItemSizes(firstColumn, minWidth + columnCellData.offset); var layerWidth = qx.lang.Array.sum(columnSizes); var layerLeft = left - columnCellData.offset; var layerRight = left - columnCellData.offset + layerWidth; } else { var firstColumn = 0; var columnSizes = []; var layerWidth = 0; var layerLeft = 0; var layerRight = 0; } this.__layerWindow = { top: layerTop, bottom: layerBottom, left: layerLeft, right: layerRight }; this.__layerContainer.setUserBounds( (this.getPaddingLeft() || 0) + (this.__layerWindow.left - this.__scrollLeft), (this.getPaddingTop() || 0) + (this.__layerWindow.top - this.__scrollTop), layerWidth, layerHeight ); this.__columnSizes = columnSizes; this.__rowSizes = rowSizes; for (var i=0; i<this.__layers.length; i++) { var layer = this.__layers[i]; layer.setUserBounds(0, 0, layerWidth, layerHeight); if (doFullUpdate) { layer.fullUpdate(firstRow, firstColumn, rowSizes, columnSizes); } else { layer.updateLayerWindow(firstRow, firstColumn, rowSizes, columnSizes); } } }, /** * Check whether the pane was resized and fire an {@link #update} event if * it was. */ __checkPaneResize : function() { if (this.__dontFireUpdate) { return; } var scrollSize = this.getScrollSize(); if ( this.__paneHeight !== scrollSize.height || this.__paneWidth !== scrollSize.width ) { this.__paneHeight = scrollSize.height; this.__paneWidth = scrollSize.width; this.fireEvent("update"); } }, /** * Schedule a full update on all visible layers. */ fullUpdate : function() { this.__jobs._fullUpdate = 1; qx.ui.core.queue.Widget.add(this); }, /** * Whether a full update is scheduled. * * @return {Boolean} Whether a full update is scheduled. */ isUpdatePending : function() { return !!this.__jobs._fullUpdate; }, /** * Perform a full update on all visible layers. All cached data will be * discarded. */ _fullUpdate : function() { var layers = this.getVisibleLayers(); if (layers.length == 0) { this.__checkPaneResize(); return; } var bounds = this.getBounds(); if (!bounds) { return; // the pane has not yet been rendered -> wait for the appear event } this._setLayerWindow( layers, this.__scrollLeft, this.__scrollTop, bounds.width, bounds.height, true ); this.__checkPaneResize(); }, /** * Schedule an update the visible window of the grid according to the top * and left scroll positions. */ _deferredUpdateScrollPosition : function() { this.__jobs._updateScrollPosition = 1; qx.ui.core.queue.Widget.add(this); }, /** * Update the visible window of the grid according to the top and left scroll * positions. */ _updateScrollPosition : function() { var layers = this.getVisibleLayers(); if (layers.length == 0) { this.__checkPaneResize(); return; } var bounds = this.getBounds(); if (!bounds) { return; // the pane has not yet been rendered -> wait for the appear event } // the visible window of the virtual coordinate space var paneWindow = { top: this.__scrollTop, bottom: this.__scrollTop + bounds.height, left: this.__scrollLeft, right: this.__scrollLeft + bounds.width }; if ( this.__layerWindow.top <= paneWindow.top && this.__layerWindow.bottom >= paneWindow.bottom && this.__layerWindow.left <= paneWindow.left && this.__layerWindow.right >= paneWindow.right ) { // only update layer container offset this.__layerContainer.setUserBounds( (this.getPaddingLeft() || 0) + (this.__layerWindow.left - paneWindow.left), (this.getPaddingTop() || 0) + (this.__layerWindow.top - paneWindow.top), this.__layerWindow.right - this.__layerWindow.left, this.__layerWindow.bottom - this.__layerWindow.top ); } else { this._setLayerWindow( layers, this.__scrollLeft, this.__scrollTop, bounds.width, bounds.height, false ); } this.__checkPaneResize(); } }, destruct : function() { this._disposeArray("__layers"); this._disposeObjects("__rowConfig", "__columnConfig", "__layerContainer"); this.__layerWindow = this.__jobs = this.__columnSizes = this.__rowSizes = null; } });