1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @constructor
7 * @extends {WebInspector.DataGrid}
8 * @param {!Array.<!WebInspector.DataGrid.ColumnDescriptor>} columnsArray
9 * @param {function(!WebInspector.DataGridNode, string, string, string)=} editCallback
10 * @param {function(!WebInspector.DataGridNode)=} deleteCallback
11 * @param {function()=} refreshCallback
12 * @param {function(!WebInspector.ContextMenu, !WebInspector.DataGridNode)=} contextMenuCallback
13 */
14WebInspector.ViewportDataGrid = function(columnsArray, editCallback, deleteCallback, refreshCallback, contextMenuCallback)
15{
16    WebInspector.DataGrid.call(this, columnsArray, editCallback, deleteCallback, refreshCallback, contextMenuCallback);
17    this._scrollContainer.addEventListener("scroll", this._onScroll.bind(this), true);
18    this._scrollContainer.addEventListener("mousewheel", this._onWheel.bind(this), true);
19    /** @type {!Array.<!WebInspector.ViewportDataGridNode>} */
20    this._visibleNodes = [];
21    /** @type {boolean} */
22    this._updateScheduled = false;
23    /** @type {boolean} */
24    this._inline = false;
25
26    // Wheel target shouldn't be removed from DOM to preserve native kinetic scrolling.
27    /** @type {?Node} */
28    this._wheelTarget = null;
29
30    // Element that was hidden earlier, but hasn't been removed yet.
31    /** @type {?Node} */
32    this._hiddenWheelTarget = null;
33
34    /** @type {boolean} */
35    this._stickToBottom = false;
36    /** @type {boolean} */
37    this._atBottom = true;
38    /** @type {number} */
39    this._lastScrollTop = 0;
40
41    this.setRootNode(new WebInspector.ViewportDataGridNode());
42}
43
44WebInspector.ViewportDataGrid.prototype = {
45    /**
46     * @override
47     */
48    onResize: function()
49    {
50        if (this._stickToBottom && this._atBottom)
51            this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight - this._scrollContainer.clientHeight;
52        this.scheduleUpdate();
53        WebInspector.DataGrid.prototype.onResize.call(this);
54    },
55
56    /**
57     * @param {boolean} stick
58     */
59    setStickToBottom: function(stick)
60    {
61        this._stickToBottom = stick;
62    },
63
64    /**
65     * @param {?Event} event
66     */
67    _onWheel: function(event)
68    {
69        this._wheelTarget = event.target ? event.target.enclosingNodeOrSelfWithNodeName("tr") : null;
70    },
71
72    /**
73     * @param {?Event} event
74     */
75    _onScroll: function(event)
76    {
77        this._atBottom = this._scrollContainer.isScrolledToBottom();
78        if (this._lastScrollTop !== this._scrollContainer.scrollTop)
79            this.scheduleUpdate();
80    },
81
82    /**
83     * @protected
84     */
85    scheduleUpdate: function()
86    {
87        if (this._updateScheduled)
88            return;
89        this._updateScheduled = true;
90        window.requestAnimationFrame(this._update.bind(this));
91    },
92
93    /**
94     * @override
95     */
96    renderInline: function()
97    {
98        this._inline = true;
99        WebInspector.DataGrid.prototype.renderInline.call(this);
100        this._update();
101    },
102
103    /**
104     * @param {number} clientHeight
105     * @param {number} scrollTop
106     * @return {{topPadding: number, bottomPadding: number, visibleNodes: !Array.<!WebInspector.ViewportDataGridNode>, offset: number}}
107     */
108    _calculateVisibleNodes: function(clientHeight, scrollTop)
109    {
110        var nodes = this._rootNode.children;
111        if (this._inline)
112            return {topPadding: 0, bottomPadding: 0, visibleNodes: nodes, offset: 0};
113
114        var size = nodes.length;
115        var i = 0;
116        var y = 0;
117
118        for (; i < size && y + nodes[i].nodeSelfHeight() < scrollTop; ++i)
119            y += nodes[i].nodeSelfHeight();
120        var start = i;
121        var topPadding = y;
122
123        for (; i < size && y < scrollTop + clientHeight; ++i)
124            y += nodes[i].nodeSelfHeight();
125        var end = i;
126
127        var bottomPadding = 0;
128        for (; i < size; ++i)
129            bottomPadding += nodes[i].nodeSelfHeight();
130
131        return {topPadding: topPadding, bottomPadding: bottomPadding, visibleNodes: nodes.slice(start, end), offset: start};
132    },
133
134    /**
135     * @return {number}
136     */
137    _contentHeight: function()
138    {
139        var nodes = this._rootNode.children;
140        var result = 0;
141        for (var i = 0, size = nodes.length; i < size; ++i)
142            result += nodes[i].nodeSelfHeight();
143        return result;
144    },
145
146    _update: function()
147    {
148        this._updateScheduled = false;
149
150        var clientHeight = this._scrollContainer.clientHeight;
151        var scrollTop = this._scrollContainer.scrollTop;
152        var currentScrollTop = scrollTop;
153        var maxScrollTop = Math.max(0, this._contentHeight() - clientHeight);
154        if (this._stickToBottom && this._atBottom)
155            scrollTop = maxScrollTop;
156        scrollTop = Math.min(maxScrollTop, scrollTop);
157        this._atBottom = scrollTop === maxScrollTop;
158
159        var viewportState = this._calculateVisibleNodes(clientHeight, scrollTop);
160        var visibleNodes = viewportState.visibleNodes;
161        var visibleNodesSet = Set.fromArray(visibleNodes);
162
163        if (this._hiddenWheelTarget && this._hiddenWheelTarget !== this._wheelTarget) {
164            this._hiddenWheelTarget.remove();
165            this._hiddenWheelTarget = null;
166        }
167
168        for (var i = 0; i < this._visibleNodes.length; ++i) {
169            var oldNode = this._visibleNodes[i];
170            if (!visibleNodesSet.contains(oldNode)) {
171                var element = oldNode.element();
172                if (element === this._wheelTarget)
173                    this._hiddenWheelTarget = oldNode.abandonElement();
174                else
175                    element.remove();
176                oldNode.wasDetached();
177            }
178        }
179
180        var previousElement = this._topFillerRow;
181        if (previousElement.nextSibling === this._hiddenWheelTarget)
182            previousElement = this._hiddenWheelTarget;
183        var tBody = this.dataTableBody;
184        var offset = viewportState.offset;
185        for (var i = 0; i < visibleNodes.length; ++i) {
186            var node = visibleNodes[i];
187            var element = node.element();
188            node.willAttach();
189            element.classList.toggle("odd", (offset + i) % 2 === 0);
190            tBody.insertBefore(element, previousElement.nextSibling);
191            previousElement = element;
192        }
193
194        this.setVerticalPadding(viewportState.topPadding, viewportState.bottomPadding);
195        this._lastScrollTop = scrollTop;
196        if (scrollTop !== currentScrollTop)
197            this._scrollContainer.scrollTop = scrollTop;
198        this._visibleNodes = visibleNodes;
199    },
200
201    /**
202     * @param {!WebInspector.ViewportDataGridNode} node
203     */
204    _revealViewportNode: function(node)
205    {
206        var nodes = this._rootNode.children;
207        var index = nodes.indexOf(node);
208        if (index === -1)
209            return;
210        var fromY = 0;
211        for (var i = 0; i < index; ++i)
212            fromY += nodes[i].nodeSelfHeight();
213        var toY = fromY + node.nodeSelfHeight();
214
215        var scrollTop = this._scrollContainer.scrollTop;
216        if (scrollTop > fromY)
217            scrollTop = fromY;
218        else if (scrollTop + this._scrollContainer.offsetHeight < toY)
219            scrollTop = toY - this._scrollContainer.offsetHeight;
220        this._scrollContainer.scrollTop = scrollTop;
221    },
222
223    __proto__: WebInspector.DataGrid.prototype
224}
225
226/**
227 * @constructor
228 * @extends {WebInspector.DataGridNode}
229 * @param {?Object.<string, *>=} data
230 */
231WebInspector.ViewportDataGridNode = function(data)
232{
233    WebInspector.DataGridNode.call(this, data, false);
234    /** @type {boolean} */
235    this._stale = false;
236}
237
238WebInspector.ViewportDataGridNode.prototype = {
239    /**
240     * @override
241     * @return {!Element}
242     */
243    element: function()
244    {
245        if (!this._element) {
246            this.createElement();
247            this.createCells();
248            this._stale = false;
249        }
250
251        if (this._stale) {
252            this.createCells();
253            this._stale = false;
254        }
255
256        return /** @type {!Element} */ (this._element);
257    },
258
259    /**
260     * @override
261     * @param {!WebInspector.DataGridNode} child
262     * @param {number} index
263     */
264    insertChild: function(child, index)
265    {
266        child.parent = this;
267        child.dataGrid = this.dataGrid;
268        this.children.splice(index, 0, child);
269        child.recalculateSiblings(index);
270        this.dataGrid.scheduleUpdate();
271    },
272
273    /**
274     * @override
275     * @param {!WebInspector.DataGridNode} child
276     */
277    removeChild: function(child)
278    {
279        child.deselect();
280        this.children.remove(child, true);
281
282        if (child.previousSibling)
283            child.previousSibling.nextSibling = child.nextSibling;
284        if (child.nextSibling)
285            child.nextSibling.previousSibling = child.previousSibling;
286
287        this.dataGrid.scheduleUpdate();
288    },
289
290    /**
291     * @override
292     */
293    removeChildren: function()
294    {
295        for (var i = 0; i < this.children.length; ++i)
296            this.children[i].deselect();
297        this.children = [];
298
299        this.dataGrid.scheduleUpdate();
300    },
301
302    /**
303     * @override
304     */
305    expand: function()
306    {
307    },
308
309    /**
310     * @protected
311     */
312    willAttach: function() { },
313
314    /**
315     * @protected
316     * @return {boolean}
317     */
318    attached: function()
319    {
320        return !!(this._element && this._element.parentElement);
321    },
322
323    /**
324     * @override
325     */
326    refresh: function()
327    {
328        if (this.attached()) {
329            this._stale = true;
330            this.dataGrid.scheduleUpdate();
331        } else {
332            this._element = null;
333        }
334    },
335
336    /**
337     * @return {?Element}
338     */
339     abandonElement: function()
340     {
341        var result = this._element;
342        if (result)
343            result.style.display = "none";
344        this._element = null;
345        return result;
346     },
347
348    reveal: function()
349    {
350        this.dataGrid._revealViewportNode(this);
351    },
352
353    __proto__: WebInspector.DataGridNode.prototype
354}
355