blob: c41921082f5f27f3d269561c3697b01e88b56c2c [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @constructor
* @extends {WebInspector.DataGrid}
* @param {!Array.<!WebInspector.DataGrid.ColumnDescriptor>} columnsArray
* @param {function(!WebInspector.DataGridNode, string, string, string)=} editCallback
* @param {function(!WebInspector.DataGridNode)=} deleteCallback
* @param {function()=} refreshCallback
* @param {function(!WebInspector.ContextMenu, !WebInspector.DataGridNode)=} contextMenuCallback
*/
WebInspector.ViewportDataGrid = function(columnsArray, editCallback, deleteCallback, refreshCallback, contextMenuCallback)
{
WebInspector.DataGrid.call(this, columnsArray, editCallback, deleteCallback, refreshCallback, contextMenuCallback);
this._scrollContainer.addEventListener("scroll", this._onScroll.bind(this), true);
this._scrollContainer.addEventListener("mousewheel", this._onWheel.bind(this), true);
/** @type {!Array.<!WebInspector.ViewportDataGridNode>} */
this._visibleNodes = [];
/** @type {boolean} */
this._updateScheduled = false;
/** @type {boolean} */
this._inline = false;
// Wheel target shouldn't be removed from DOM to preserve native kinetic scrolling.
/** @type {?Node} */
this._wheelTarget = null;
// Element that was hidden earlier, but hasn't been removed yet.
/** @type {?Node} */
this._hiddenWheelTarget = null;
/** @type {boolean} */
this._stickToBottom = false;
/** @type {boolean} */
this._atBottom = true;
/** @type {number} */
this._lastScrollTop = 0;
this.setRootNode(new WebInspector.ViewportDataGridNode());
}
WebInspector.ViewportDataGrid.prototype = {
/**
* @override
*/
onResize: function()
{
if (this._stickToBottom && this._atBottom)
this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight - this._scrollContainer.clientHeight;
this.scheduleUpdate();
},
/**
* @param {boolean} stick
*/
setStickToBottom: function(stick)
{
this._stickToBottom = stick;
},
/**
* @param {?Event} event
*/
_onWheel: function(event)
{
this._wheelTarget = event.target ? event.target.enclosingNodeOrSelfWithNodeName("tr") : null;
},
/**
* @param {?Event} event
*/
_onScroll: function(event)
{
this._atBottom = this._scrollContainer.isScrolledToBottom();
if (this._lastScrollTop !== this._scrollContainer.scrollTop)
this.scheduleUpdate();
},
/**
* @protected
*/
scheduleUpdate: function()
{
if (this._updateScheduled)
return;
this._updateScheduled = true;
window.requestAnimationFrame(this._update.bind(this));
},
/**
* @override
*/
renderInline: function()
{
this._inline = true;
WebInspector.DataGrid.prototype.renderInline.call(this);
this._update();
},
/**
* @param {number} clientHeight
* @param {number} scrollTop
* @return {{topPadding: number, bottomPadding: number, visibleNodes: !Array.<!WebInspector.ViewportDataGridNode>, offset: number}}
*/
_calculateVisibleNodes: function(clientHeight, scrollTop)
{
var nodes = this._rootNode.children;
if (this._inline)
return {topPadding: 0, bottomPadding: 0, visibleNodes: nodes, offset: 0};
var size = nodes.length;
var i = 0;
var y = 0;
for (; i < size && y + nodes[i].nodeSelfHeight() < scrollTop; ++i)
y += nodes[i].nodeSelfHeight();
var start = i;
var topPadding = y;
for (; i < size && y < scrollTop + clientHeight; ++i)
y += nodes[i].nodeSelfHeight();
var end = i;
var bottomPadding = 0;
for (; i < size; ++i)
bottomPadding += nodes[i].nodeSelfHeight();
return {topPadding: topPadding, bottomPadding: bottomPadding, visibleNodes: nodes.slice(start, end), offset: start};
},
/**
* @return {number}
*/
_contentHeight: function()
{
var nodes = this._rootNode.children;
var result = 0;
for (var i = 0, size = nodes.length; i < size; ++i)
result += nodes[i].nodeSelfHeight();
return result;
},
_update: function()
{
this._updateScheduled = false;
var clientHeight = this._scrollContainer.clientHeight;
var scrollTop = this._scrollContainer.scrollTop;
var currentScrollTop = scrollTop;
var maxScrollTop = Math.max(0, this._contentHeight() - clientHeight);
if (this._stickToBottom && this._atBottom)
scrollTop = maxScrollTop;
scrollTop = Math.min(maxScrollTop, scrollTop);
this._atBottom = scrollTop === maxScrollTop;
var viewportState = this._calculateVisibleNodes(clientHeight, scrollTop);
var visibleNodes = viewportState.visibleNodes;
var visibleNodesSet = Set.fromArray(visibleNodes);
if (this._hiddenWheelTarget && this._hiddenWheelTarget !== this._wheelTarget) {
this._hiddenWheelTarget.remove();
this._hiddenWheelTarget = null;
}
for (var i = 0; i < this._visibleNodes.length; ++i) {
var oldNode = this._visibleNodes[i];
if (!visibleNodesSet.contains(oldNode)) {
var element = oldNode.element();
if (element === this._wheelTarget)
this._hiddenWheelTarget = oldNode.abandonElement();
else
element.remove();
oldNode.wasDetached();
}
}
var previousElement = this._topFillerRow;
if (previousElement.nextSibling === this._hiddenWheelTarget)
previousElement = this._hiddenWheelTarget;
var tBody = this.dataTableBody;
var offset = viewportState.offset;
for (var i = 0; i < visibleNodes.length; ++i) {
var element = visibleNodes[i].element();
element.classList.toggle("odd", (offset + i) % 2 === 0);
tBody.insertBefore(element, previousElement.nextSibling);
previousElement = element;
}
this.setVerticalPadding(viewportState.topPadding, viewportState.bottomPadding);
this._lastScrollTop = scrollTop;
if (scrollTop !== currentScrollTop)
this._scrollContainer.scrollTop = scrollTop;
this._visibleNodes = visibleNodes;
},
/**
* @param {!WebInspector.ViewportDataGridNode} node
*/
_revealViewportNode: function(node)
{
var nodes = this._rootNode.children;
var index = nodes.indexOf(node);
if (index === -1)
return;
var fromY = 0;
for (var i = 0; i < index; ++i)
fromY += nodes[i].nodeSelfHeight();
var toY = fromY + node.nodeSelfHeight();
var scrollTop = this._scrollContainer.scrollTop;
if (scrollTop > fromY)
scrollTop = fromY;
else if (scrollTop + this._scrollContainer.offsetHeight < toY)
scrollTop = toY - this._scrollContainer.offsetHeight;
this._scrollContainer.scrollTop = scrollTop;
},
__proto__: WebInspector.DataGrid.prototype
}
/**
* @constructor
* @extends {WebInspector.DataGridNode}
* @param {?Object.<string, *>=} data
*/
WebInspector.ViewportDataGridNode = function(data)
{
WebInspector.DataGridNode.call(this, data, false);
/** @type {boolean} */
this._stale = false;
}
WebInspector.ViewportDataGridNode.prototype = {
/**
* @override
* @return {!Element}
*/
element: function()
{
if (!this._element) {
this.createElement();
this.createCells();
this._stale = false;
}
if (this._stale) {
this.createCells();
this._stale = false;
}
return /** @type {!Element} */ (this._element);
},
/**
* @override
* @param {!WebInspector.DataGridNode} child
* @param {number} index
*/
insertChild: function(child, index)
{
child.parent = this;
child.dataGrid = this.dataGrid;
this.children.splice(index, 0, child);
child.recalculateSiblings(index);
this.dataGrid.scheduleUpdate();
},
/**
* @override
* @param {!WebInspector.DataGridNode} child
*/
removeChild: function(child)
{
child.deselect();
this.children.remove(child, true);
if (child.previousSibling)
child.previousSibling.nextSibling = child.nextSibling;
if (child.nextSibling)
child.nextSibling.previousSibling = child.previousSibling;
this.dataGrid.scheduleUpdate();
},
/**
* @override
*/
removeChildren: function()
{
for (var i = 0; i < this.children.length; ++i)
this.children[i].deselect();
this.children = [];
this.dataGrid.scheduleUpdate();
},
/**
* @override
*/
expand: function()
{
},
/**
* @override
*/
refresh: function()
{
if (this._element && this._element.parentElement) {
this._stale = true;
this.dataGrid.scheduleUpdate();
} else {
this._element = null;
}
},
/**
* @return {?Element}
*/
abandonElement: function()
{
var result = this._element;
if (result)
result.style.display = "none";
this._element = null;
return result;
},
reveal: function()
{
this.dataGrid._revealViewportNode(this);
},
__proto__: WebInspector.DataGridNode.prototype
}