@qooxdoo/framework
Version:
The JS Framework for Coders
981 lines (820 loc) • 26.7 kB
JavaScript
/* ************************************************************************
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;
}
});