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