Home | History | Annotate | Download | only in ui
      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  */
     14 WebInspector.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 
     44 WebInspector.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  */
    231 WebInspector.ViewportDataGridNode = function(data)
    232 {
    233     WebInspector.DataGridNode.call(this, data, false);
    234     /** @type {boolean} */
    235     this._stale = false;
    236 }
    237 
    238 WebInspector.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