1/* 2 * Copyright (C) 2008, 2013, 2014 Apple Inc. All Rights Reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26WebInspector.DataGrid = function(columnsData, editCallback, deleteCallback) 27{ 28 this.columns = new Map; 29 this.orderedColumns = []; 30 31 this._sortColumnIdentifier = null; 32 this._sortOrder = WebInspector.DataGrid.SortOrder.Indeterminate; 33 34 this.children = []; 35 this.selectedNode = null; 36 this.expandNodesWhenArrowing = false; 37 this.root = true; 38 this.hasChildren = false; 39 this.expanded = true; 40 this.revealed = true; 41 this.selected = false; 42 this.dataGrid = this; 43 this.indentWidth = 15; 44 this.resizerElements = []; 45 this._columnWidthsInitialized = false; 46 47 this.element = document.createElement("div"); 48 this.element.className = "data-grid"; 49 this.element.tabIndex = 0; 50 this.element.addEventListener("keydown", this._keyDown.bind(this), false); 51 this.element.copyHandler = this; 52 53 this._headerTableElement = document.createElement("table"); 54 this._headerTableElement.className = "header"; 55 this._headerTableColumnGroupElement = this._headerTableElement.createChild("colgroup"); 56 this._headerTableBodyElement = this._headerTableElement.createChild("tbody"); 57 this._headerTableRowElement = this._headerTableBodyElement.createChild("tr"); 58 this._headerTableCellElements = new Map; 59 60 this._scrollContainerElement = document.createElement("div"); 61 this._scrollContainerElement.className = "data-container"; 62 63 this._dataTableElement = this._scrollContainerElement.createChild("table"); 64 this._dataTableElement.className = "data"; 65 66 this._dataTableElement.addEventListener("mousedown", this._mouseDownInDataTable.bind(this)); 67 this._dataTableElement.addEventListener("click", this._clickInDataTable.bind(this)); 68 this._dataTableElement.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true); 69 70 // FIXME: Add a createCallback which is different from editCallback and has different 71 // behavior when creating a new node. 72 if (editCallback) { 73 this._dataTableElement.addEventListener("dblclick", this._ondblclick.bind(this), false); 74 this._editCallback = editCallback; 75 } 76 if (deleteCallback) 77 this._deleteCallback = deleteCallback; 78 79 this._dataTableColumnGroupElement = this._headerTableColumnGroupElement.cloneNode(true); 80 this._dataTableElement.appendChild(this._dataTableColumnGroupElement); 81 82 // This element is used by DataGridNodes to manipulate table rows and cells. 83 this.dataTableBodyElement = this._dataTableElement.createChild("tbody"); 84 this._fillerRowElement = this.dataTableBodyElement.createChild("tr"); 85 this._fillerRowElement.className = "filler"; 86 87 this.element.appendChild(this._headerTableElement); 88 this.element.appendChild(this._scrollContainerElement); 89 90 for (var columnIdentifier in columnsData) 91 this.insertColumn(columnIdentifier, columnsData[columnIdentifier]); 92 93 this._generateSortIndicatorImagesIfNeeded(); 94} 95 96WebInspector.DataGrid.Event = { 97 DidLayout: "datagrid-did-layout", 98 SortChanged: "datagrid-sort-changed", 99 SelectedNodeChanged: "datagrid-selected-node-changed", 100 ExpandedNode: "datagrid-expanded-node", 101 CollapsedNode: "datagrid-collapsed-node" 102}; 103 104WebInspector.DataGrid.SortOrder = { 105 Indeterminate: "data-grid-sort-order-indeterminate", 106 Ascending: "data-grid-sort-order-ascending", 107 Descending: "data-grid-sort-order-descending" 108}; 109 110WebInspector.DataGrid.SortColumnAscendingStyleClassName = "sort-ascending"; 111WebInspector.DataGrid.SortColumnDescendingStyleClassName = "sort-descending"; 112WebInspector.DataGrid.SortableColumnStyleClassName = "sortable"; 113 114WebInspector.DataGrid.createSortableDataGrid = function(columnNames, values) 115{ 116 var numColumns = columnNames.length; 117 if (!numColumns) 118 return null; 119 120 var columnsData = {}; 121 122 for (var columnName of columnNames) { 123 var column = {}; 124 column["width"] = columnName.length; 125 column["title"] = columnName; 126 column["sortable"] = true; 127 128 columnsData[columnName] = column; 129 } 130 131 var dataGrid = new WebInspector.DataGrid(columnsData); 132 for (var i = 0; i < values.length / numColumns; ++i) { 133 var data = {}; 134 for (var j = 0; j < columnNames.length; ++j) 135 data[columnNames[j]] = values[numColumns * i + j]; 136 137 var node = new WebInspector.DataGridNode(data, false); 138 node.selectable = false; 139 dataGrid.appendChild(node); 140 } 141 142 function sortDataGrid() 143 { 144 var sortColumnIdentifier = dataGrid.sortColumnIdentifier; 145 var sortAscending = dataGrid.sortOrder === WebInspector.DataGrid.SortOrder.Ascending ? 1 : -1; 146 147 for (var node of dataGrid.children) { 148 if (isNaN(Number(node.data[sortColumnIdentifier] || ""))) 149 columnIsNumeric = false; 150 } 151 152 function comparator(dataGridNode1, dataGridNode2) 153 { 154 var item1 = dataGridNode1.data[sortColumnIdentifier] || ""; 155 var item2 = dataGridNode2.data[sortColumnIdentifier] || ""; 156 157 var comparison; 158 if (columnIsNumeric) { 159 // Sort numbers based on comparing their values rather than a lexicographical comparison. 160 var number1 = parseFloat(item1); 161 var number2 = parseFloat(item2); 162 comparison = number1 < number2 ? -1 : (number1 > number2 ? 1 : 0); 163 } else 164 comparison = item1 < item2 ? -1 : (item1 > item2 ? 1 : 0); 165 166 return sortDirection * comparison; 167 } 168 169 dataGrid.sortNodes(comparator); 170 } 171 172 dataGrid.addEventListener(WebInspector.DataGrid.Event.SortChanged, sortDataGrid, this); 173 return dataGrid; 174} 175 176WebInspector.DataGrid.prototype = { 177 get refreshCallback() 178 { 179 return this._refreshCallback; 180 }, 181 182 set refreshCallback(refreshCallback) 183 { 184 this._refreshCallback = refreshCallback; 185 }, 186 187 get sortOrder() 188 { 189 return this._sortOrder; 190 }, 191 192 set sortOrder(order) 193 { 194 if (order === this._sortOrder) 195 return; 196 197 this._sortOrder = order; 198 199 if (!this._sortColumnIdentifier) 200 return; 201 202 var sortHeaderCellElement = this._headerTableCellElements.get(this._sortColumnIdentifier); 203 204 sortHeaderCellElement.classList.toggle(WebInspector.DataGrid.SortColumnAscendingStyleClassName, this._sortOrder === WebInspector.DataGrid.SortOrder.Ascending); 205 sortHeaderCellElement.classList.toggle(WebInspector.DataGrid.SortColumnDescendingStyleClassName, this._sortOrder === WebInspector.DataGrid.SortOrder.Descending); 206 207 this.dispatchEventToListeners(WebInspector.DataGrid.Event.SortChanged); 208 }, 209 210 get sortColumnIdentifier() 211 { 212 return this._sortColumnIdentifier; 213 }, 214 215 set sortColumnIdentifier(columnIdentifier) 216 { 217 console.assert(columnIdentifier && this.columns.has(columnIdentifier)); 218 console.assert("sortable" in this.columns.get(columnIdentifier)); 219 220 if (this._sortColumnIdentifier === columnIdentifier) 221 return; 222 223 var oldSortColumnIdentifier = this._sortColumnIdentifier; 224 this._sortColumnIdentifier = columnIdentifier; 225 226 if (oldSortColumnIdentifier) { 227 var oldSortHeaderCellElement = this._headerTableCellElements.get(oldSortColumnIdentifier); 228 oldSortHeaderCellElement.classList.remove(WebInspector.DataGrid.SortColumnAscendingStyleClassName); 229 oldSortHeaderCellElement.classList.remove(WebInspector.DataGrid.SortColumnDescendingStyleClassName); 230 } 231 232 if (this._sortColumnIdentifier) { 233 var newSortHeaderCellElement = this._headerTableCellElements.get(this._sortColumnIdentifier); 234 newSortHeaderCellElement.classList.toggle(WebInspector.DataGrid.SortColumnAscendingStyleClassName, this._sortOrder === WebInspector.DataGrid.SortOrder.Ascending); 235 newSortHeaderCellElement.classList.toggle(WebInspector.DataGrid.SortColumnDescendingStyleClassName, this._sortOrder === WebInspector.DataGrid.SortOrder.Descending); 236 } 237 238 this.dispatchEventToListeners(WebInspector.DataGrid.Event.SortChanged); 239 }, 240 241 _ondblclick: function(event) 242 { 243 if (this._editing || this._editingNode) 244 return; 245 246 this._startEditing(event.target); 247 }, 248 249 _startEditingNodeAtColumnIndex: function(node, columnIndex) 250 { 251 console.assert(node, "Invalid argument: must provide DataGridNode to edit."); 252 253 this._editing = true; 254 this._editingNode = node; 255 this._editingNode.select(); 256 257 var element = this._editingNode._element.children[columnIndex]; 258 WebInspector.startEditing(element, this._startEditingConfig(element)); 259 window.getSelection().setBaseAndExtent(element, 0, element, 1); 260 }, 261 262 _startEditing: function(target) 263 { 264 var element = target.enclosingNodeOrSelfWithNodeName("td"); 265 if (!element) 266 return; 267 268 this._editingNode = this.dataGridNodeFromNode(target); 269 if (!this._editingNode) { 270 if (!this.placeholderNode) 271 return; 272 this._editingNode = this.placeholderNode; 273 } 274 275 // Force editing the 1st column when editing the placeholder node 276 if (this._editingNode.isPlaceholderNode) 277 return this._startEditingNodeAtColumnIndex(this._editingNode, 0); 278 279 this._editing = true; 280 WebInspector.startEditing(element, this._startEditingConfig(element)); 281 282 window.getSelection().setBaseAndExtent(element, 0, element, 1); 283 }, 284 285 _startEditingConfig: function(element) 286 { 287 return new WebInspector.EditingConfig(this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent); 288 }, 289 290 _editingCommitted: function(element, newText, oldText, context, moveDirection) 291 { 292 var columnIdentifier = element.__columnIdentifier; 293 var columnIndex = this.orderedColumns.indexOf(columnIdentifier); 294 295 var textBeforeEditing = this._editingNode.data[columnIdentifier] || ""; 296 var currentEditingNode = this._editingNode; 297 298 // Returns an object with the next node and column index to edit, and whether it 299 // is an appropriate time to re-sort the table rows. When editing, we want to 300 // postpone sorting until we switch rows or wrap around a row. 301 function determineNextCell(valueDidChange) { 302 if (moveDirection === "forward") { 303 if (columnIndex < this.orderedColumns.length - 1) 304 return {shouldSort: false, editingNode: currentEditingNode, columnIndex: columnIndex + 1}; 305 306 // Continue by editing the first column of the next row if it exists. 307 var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true); 308 return {shouldSort: true, editingNode: nextDataGridNode || currentEditingNode, columnIndex: 0}; 309 } 310 311 if (moveDirection === "backward") { 312 if (columnIndex > 0) 313 return {shouldSort: false, editingNode: currentEditingNode, columnIndex: columnIndex - 1}; 314 315 var previousDataGridNode = currentEditingNode.traversePreviousNode(true, null, true); 316 return {shouldSort: true, editingNode: previousDataGridNode || currentEditingNode, columnIndex: this.orderedColumns.length - 1}; 317 } 318 319 // If we are not moving in any direction, then sort but don't move. 320 return {shouldSort: true, editingNode: currentEditingNode, columnIndex: columnIndex}; 321 } 322 323 function moveToNextCell(valueDidChange) { 324 var moveCommand = determineNextCell.call(this, valueDidChange); 325 if (moveCommand.shouldSort && this._sortAfterEditingCallback) { 326 this._sortAfterEditingCallback(); 327 delete this._sortAfterEditingCallback; 328 } 329 this._startEditingNodeAtColumnIndex(moveCommand.editingNode, moveCommand.columnIndex); 330 } 331 332 this._editingCancelled(element); 333 334 // Update table's data model, and delegate to the callback to update other models. 335 currentEditingNode.data[columnIdentifier] = newText.trim(); 336 this._editCallback(currentEditingNode, columnIdentifier, textBeforeEditing, newText, moveDirection); 337 338 var textDidChange = textBeforeEditing.trim() !== newText.trim(); 339 moveToNextCell.call(this, textDidChange); 340 }, 341 342 _editingCancelled: function(element) 343 { 344 console.assert(this._editingNode.element === element.enclosingNodeOrSelfWithNodeName("tr")); 345 delete this._editing; 346 this._editingNode = null; 347 }, 348 349 autoSizeColumns: function(minPercent, maxPercent, maxDescentLevel) 350 { 351 if (minPercent) 352 minPercent = Math.min(minPercent, Math.floor(100 / this.orderedColumns.length)); 353 var widths = {}; 354 // For the first width approximation, use the character length of column titles. 355 for (var [identifier, column] of this.columns) 356 widths[identifier] = (column["title"] || "").length; 357 358 // Now approximate the width of each column as max(title, cells). 359 var children = maxDescentLevel ? this._enumerateChildren(this, [], maxDescentLevel + 1) : this.children; 360 for (var node of children) { 361 for (var identifier of this.columns.keys()) { 362 var text = node.data[identifier] || ""; 363 if (text.length > widths[identifier]) 364 widths[identifier] = text.length; 365 } 366 } 367 368 var totalColumnWidths = 0; 369 for (var identifier of this.columns.keys()) 370 totalColumnWidths += widths[identifier]; 371 372 // Compute percentages and clamp desired widths to min and max widths. 373 var recoupPercent = 0; 374 for (var identifier of this.columns.keys()) { 375 var width = Math.round(100 * widths[identifier] / totalColumnWidths); 376 if (minPercent && width < minPercent) { 377 recoupPercent += (minPercent - width); 378 width = minPercent; 379 } else if (maxPercent && width > maxPercent) { 380 recoupPercent -= (width - maxPercent); 381 width = maxPercent; 382 } 383 widths[identifier] = width; 384 } 385 386 // If we assigned too much width due to the above, reduce column widths. 387 while (minPercent && recoupPercent > 0) { 388 for (var identifier of this.columns.keys()) { 389 if (widths[identifier] > minPercent) { 390 --widths[identifier]; 391 --recoupPercent; 392 if (!recoupPercent) 393 break; 394 } 395 } 396 } 397 398 // If extra width remains after clamping widths, expand column widths. 399 while (maxPercent && recoupPercent < 0) { 400 for (var identifier of this.columns.keys()) { 401 if (widths[identifier] < maxPercent) { 402 ++widths[identifier]; 403 ++recoupPercent; 404 if (!recoupPercent) 405 break; 406 } 407 } 408 } 409 410 for (var [identifier, column] of this.columns) 411 column["element"].style.width = widths[identifier] + "%"; 412 this._columnWidthsInitialized = false; 413 this.updateLayout(); 414 }, 415 416 insertColumn: function(columnIdentifier, columnData, insertionIndex) { 417 if (typeof insertionIndex === "undefined") 418 insertionIndex = this.orderedColumns.length; 419 insertionIndex = Number.constrain(insertionIndex, 0, this.orderedColumns.length); 420 421 var listeners = new WebInspector.EventListenerSet(this, "DataGrid column DOM listeners"); 422 423 // Copy configuration properties instead of keeping a reference to the passed-in object. 424 var column = Object.shallowCopy(columnData); 425 column["listeners"] = listeners; 426 column["ordinal"] = insertionIndex; 427 column["columnIdentifier"] = columnIdentifier; 428 429 this.orderedColumns.splice(insertionIndex, 0, columnIdentifier); 430 431 for (var [identifier, existingColumn] of this.columns) { 432 var ordinal = existingColumn["ordinal"]; 433 if (ordinal >= insertionIndex) // Also adjust the "old" column at insertion index. 434 existingColumn["ordinal"] = ordinal + 1; 435 } 436 this.columns.set(columnIdentifier, column); 437 438 if (column["disclosure"]) 439 this.disclosureColumnIdentifier = columnIdentifier; 440 441 var headerColumnElement = document.createElement("col"); 442 if (column["width"]) 443 headerColumnElement.style.width = column["width"]; 444 column["element"] = headerColumnElement; 445 var referenceElement = this._headerTableColumnGroupElement.children[insertionIndex]; 446 this._headerTableColumnGroupElement.insertBefore(headerColumnElement, referenceElement); 447 448 var headerCellElement = document.createElement("th"); 449 headerCellElement.className = columnIdentifier + "-column"; 450 headerCellElement.columnIdentifier = columnIdentifier; 451 if (column["aligned"]) 452 headerCellElement.classList.add(column["aligned"]); 453 this._headerTableCellElements.set(columnIdentifier, headerCellElement); 454 var referenceElement = this._headerTableRowElement.children[insertionIndex]; 455 this._headerTableRowElement.insertBefore(headerCellElement, referenceElement); 456 457 var div = headerCellElement.createChild("div"); 458 if (column["titleDOMFragment"]) 459 div.appendChild(column["titleDOMFragment"]); 460 else 461 div.textContent = column["title"] || ""; 462 463 if (column["sortable"]) { 464 listeners.register(headerCellElement, "click", this._headerCellClicked); 465 headerCellElement.classList.add(WebInspector.DataGrid.SortableColumnStyleClassName); 466 } 467 468 if (column["group"]) 469 headerCellElement.classList.add("column-group-" + column["group"]); 470 471 if (column["collapsesGroup"]) { 472 console.assert(column["group"] !== column["collapsesGroup"]); 473 474 var dividerElement = headerCellElement.createChild("div"); 475 dividerElement.className = "divider"; 476 477 var collapseDiv = headerCellElement.createChild("div"); 478 collapseDiv.className = "collapser-button"; 479 collapseDiv.title = this._collapserButtonCollapseColumnsToolTip(); 480 listeners.register(collapseDiv, "mouseover", this._mouseoverColumnCollapser); 481 listeners.register(collapseDiv, "mouseout", this._mouseoutColumnCollapser); 482 listeners.register(collapseDiv, "click", this._clickInColumnCollapser); 483 484 headerCellElement.collapsesGroup = column["collapsesGroup"]; 485 headerCellElement.classList.add("collapser"); 486 } 487 488 this._headerTableColumnGroupElement.span = this.orderedColumns.length; 489 490 var dataColumnElement = headerColumnElement.cloneNode(); 491 var referenceElement = this._dataTableColumnGroupElement.children[insertionIndex]; 492 this._dataTableColumnGroupElement.insertBefore(dataColumnElement, referenceElement); 493 column["bodyElement"] = dataColumnElement; 494 495 var fillerCellElement = document.createElement("td"); 496 fillerCellElement.className = columnIdentifier + "-column"; 497 fillerCellElement.__columnIdentifier = columnIdentifier; 498 if (column["group"]) 499 fillerCellElement.classList.add("column-group-" + column["group"]); 500 var referenceElement = this._fillerRowElement.children[insertionIndex]; 501 this._fillerRowElement.insertBefore(fillerCellElement, referenceElement); 502 503 listeners.install(); 504 505 if (column["hidden"]) 506 this._hideColumn(columnIdentifier); 507 }, 508 509 removeColumn: function(columnIdentifier) 510 { 511 console.assert(this.columns.has(columnIdentifier)); 512 var removedColumn = this.columns.get(columnIdentifier); 513 this.columns.delete(columnIdentifier); 514 this.orderedColumns.splice(this.orderedColumns.indexOf(columnIdentifier), 1); 515 516 var removedOrdinal = removedColumn["ordinal"]; 517 for (var [identifier, column] of this.columns) { 518 var ordinal = column["ordinal"]; 519 if (ordinal > removedOrdinal) 520 column["ordinal"] = ordinal - 1; 521 } 522 523 removedColumn["listeners"].uninstall(true); 524 525 if (removedColumn["disclosure"]) 526 delete this.disclosureColumnIdentifier; 527 528 if (this.sortColumnIdentifier === columnIdentifier) 529 this.sortColumnIdentifier = null; 530 531 this._headerTableCellElements.delete(columnIdentifier); 532 this._headerTableRowElement.children[removedOrdinal].remove(); 533 this._headerTableColumnGroupElement.children[removedOrdinal].remove(); 534 this._dataTableColumnGroupElement.children[removedOrdinal].remove(); 535 this._fillerRowElement.children[removedOrdinal].remove(); 536 537 this._headerTableColumnGroupElement.span = this.orderedColumns.length; 538 539 for (var child of this.children) 540 child.refresh(); 541 }, 542 543 _enumerateChildren: function(rootNode, result, maxLevel) 544 { 545 if (!rootNode.root) 546 result.push(rootNode); 547 if (!maxLevel) 548 return; 549 for (var i = 0; i < rootNode.children.length; ++i) 550 this._enumerateChildren(rootNode.children[i], result, maxLevel - 1); 551 return result; 552 }, 553 554 // Updates the widths of the table, including the positions of the column 555 // resizers. 556 // 557 // IMPORTANT: This function MUST be called once after the element of the 558 // DataGrid is attached to its parent element and every subsequent time the 559 // width of the parent element is changed in order to make it possible to 560 // resize the columns. 561 // 562 // If this function is not called after the DataGrid is attached to its 563 // parent element, then the DataGrid's columns will not be resizable. 564 updateLayout: function() 565 { 566 // Do not attempt to use offsetes if we're not attached to the document tree yet. 567 if (!this._columnWidthsInitialized && this.element.offsetWidth) { 568 // Give all the columns initial widths now so that during a resize, 569 // when the two columns that get resized get a percent value for 570 // their widths, all the other columns already have percent values 571 // for their widths. 572 var headerTableColumnElements = this._headerTableColumnGroupElement.children; 573 var tableWidth = this._dataTableElement.offsetWidth; 574 var numColumns = headerTableColumnElements.length; 575 for (var i = 0; i < numColumns; i++) { 576 var headerCellElement = this._headerTableBodyElement.rows[0].cells[i] 577 if (this._isColumnVisible(headerCellElement.columnIdentifier)) { 578 var columnWidth = headerCellElement.offsetWidth; 579 var percentWidth = ((columnWidth / tableWidth) * 100) + "%"; 580 this._headerTableColumnGroupElement.children[i].style.width = percentWidth; 581 this._dataTableColumnGroupElement.children[i].style.width = percentWidth; 582 } else { 583 this._headerTableColumnGroupElement.children[i].style.width = 0; 584 this._dataTableColumnGroupElement.children[i].style.width = 0; 585 } 586 } 587 588 this._columnWidthsInitialized = true; 589 } 590 591 this._positionResizerElements(); 592 this.dispatchEventToListeners(WebInspector.DataGrid.Event.DidLayout); 593 }, 594 595 columnWidthsMap: function() 596 { 597 var result = {}; 598 for (var [identifier, column] of this.columns) { 599 var width = this._headerTableColumnGroupElement.children[column["ordinal"]].style.width; 600 result[columnIdentifier] = parseFloat(width); 601 } 602 return result; 603 }, 604 605 applyColumnWidthsMap: function(columnWidthsMap) 606 { 607 for (var [identifier, column] of this.columns) { 608 var width = (columnWidthsMap[identifier] || 0) + "%"; 609 var ordinal = column["ordinal"]; 610 this._headerTableColumnGroupElement.children[ordinal].style.width = width; 611 this._dataTableColumnGroupElement.children[ordinal].style.width = width; 612 } 613 614 this.updateLayout(); 615 }, 616 617 _isColumnVisible: function(columnIdentifier) 618 { 619 return !this.columns.get(columnIdentifier)["hidden"]; 620 }, 621 622 _showColumn: function(columnIdentifier) 623 { 624 delete this.columns.get(columnIdentifier)["hidden"]; 625 }, 626 627 _hideColumn: function(columnIdentifier) 628 { 629 var column = this.columns.get(columnIdentifier); 630 column["hidden"] = true; 631 632 var columnElement = column["element"]; 633 columnElement.style.width = 0; 634 635 this._columnWidthsInitialized = false; 636 }, 637 638 get scrollContainer() 639 { 640 return this._scrollContainerElement; 641 }, 642 643 isScrolledToLastRow: function() 644 { 645 return this._scrollContainerElement.isScrolledToBottom(); 646 }, 647 648 scrollToLastRow: function() 649 { 650 this._scrollContainerElement.scrollTop = this._scrollContainerElement.scrollHeight - this._scrollContainerElement.offsetHeight; 651 }, 652 653 _positionResizerElements: function() 654 { 655 var left = 0; 656 var previousResizerElement = null; 657 658 // Make n - 1 resizers for n columns. 659 for (var i = 0; i < this.orderedColumns.length - 1; ++i) { 660 var resizerElement = this.resizerElements[i]; 661 662 if (!resizerElement) { 663 // This is the first call to updateWidth, so the resizers need 664 // to be created. 665 resizerElement = document.createElement("div"); 666 resizerElement.classList.add("data-grid-resizer"); 667 // This resizer is associated with the column to its right. 668 resizerElement.addEventListener("mousedown", this._startResizerDragging.bind(this), false); 669 this.element.appendChild(resizerElement); 670 this.resizerElements[i] = resizerElement; 671 } 672 673 // Get the width of the cell in the first (and only) row of the 674 // header table in order to determine the width of the column, since 675 // it is not possible to query a column for its width. 676 left += this._headerTableBodyElement.rows[0].cells[i].offsetWidth; 677 678 if (this._isColumnVisible(this.orderedColumns[i])) { 679 resizerElement.style.removeProperty("display"); 680 resizerElement.style.left = left + "px"; 681 resizerElement.leftNeighboringColumnID = i; 682 if (previousResizerElement) 683 previousResizerElement.rightNeighboringColumnID = i; 684 previousResizerElement = resizerElement; 685 } else { 686 resizerElement.style.setProperty("display", "none"); 687 resizerElement.leftNeighboringColumnID = 0; 688 resizerElement.rightNeighboringColumnID = 0; 689 } 690 } 691 if (previousResizerElement) 692 previousResizerElement.rightNeighboringColumnID = this.orderedColumns.length - 1; 693 }, 694 695 addPlaceholderNode: function() 696 { 697 if (this.placeholderNode) 698 this.placeholderNode.makeNormal(); 699 700 var emptyData = {}; 701 for (var identifier of this.columns.keys()) 702 emptyData[identifier] = ""; 703 this.placeholderNode = new WebInspector.PlaceholderDataGridNode(emptyData); 704 this.appendChild(this.placeholderNode); 705 }, 706 707 appendChild: function(child) 708 { 709 this.insertChild(child, this.children.length); 710 }, 711 712 insertChild: function(child, index) 713 { 714 if (!child) 715 throw("insertChild: Node can't be undefined or null."); 716 if (child.parent === this) 717 throw("insertChild: Node is already a child of this node."); 718 719 if (child.parent) 720 child.parent.removeChild(child); 721 722 this.children.splice(index, 0, child); 723 this.hasChildren = true; 724 725 child.parent = this; 726 child.dataGrid = this.dataGrid; 727 child._recalculateSiblings(index); 728 729 delete child._depth; 730 delete child._revealed; 731 delete child._attached; 732 child._shouldRefreshChildren = true; 733 734 var current = child.children[0]; 735 while (current) { 736 current.dataGrid = this.dataGrid; 737 delete current._depth; 738 delete current._revealed; 739 delete current._attached; 740 current._shouldRefreshChildren = true; 741 current = current.traverseNextNode(false, child, true); 742 } 743 744 if (this.expanded) 745 child._attach(); 746 }, 747 748 removeChild: function(child) 749 { 750 if (!child) 751 throw("removeChild: Node can't be undefined or null."); 752 if (child.parent !== this) 753 throw("removeChild: Node is not a child of this node."); 754 755 child.deselect(); 756 child._detach(); 757 758 this.children.remove(child, true); 759 760 if (child.previousSibling) 761 child.previousSibling.nextSibling = child.nextSibling; 762 if (child.nextSibling) 763 child.nextSibling.previousSibling = child.previousSibling; 764 765 child.dataGrid = null; 766 child.parent = null; 767 child.nextSibling = null; 768 child.previousSibling = null; 769 770 if (this.children.length <= 0) 771 this.hasChildren = false; 772 773 console.assert(!child.isPlaceholderNode, "Shouldn't delete the placeholder node."); 774 }, 775 776 removeChildren: function() 777 { 778 for (var i = 0; i < this.children.length; ++i) { 779 var child = this.children[i]; 780 child.deselect(); 781 child._detach(); 782 783 child.dataGrid = null; 784 child.parent = null; 785 child.nextSibling = null; 786 child.previousSibling = null; 787 } 788 789 this.children = []; 790 this.hasChildren = false; 791 }, 792 793 removeChildrenRecursive: function() 794 { 795 var childrenToRemove = this.children; 796 797 var child = this.children[0]; 798 while (child) { 799 if (child.children.length) 800 childrenToRemove = childrenToRemove.concat(child.children); 801 child = child.traverseNextNode(false, this, true); 802 } 803 804 for (var i = 0; i < childrenToRemove.length; ++i) { 805 child = childrenToRemove[i]; 806 child.deselect(); 807 child._detach(); 808 809 child.children = []; 810 child.dataGrid = null; 811 child.parent = null; 812 child.nextSibling = null; 813 child.previousSibling = null; 814 } 815 816 this.children = []; 817 }, 818 819 sortNodes: function(comparator) 820 { 821 if (this._sortNodesRequestId) 822 return; 823 824 this._sortNodesRequestId = window.requestAnimationFrame(this._sortNodesCallback.bind(this, comparator)); 825 }, 826 827 _sortNodesCallback: function(comparator) 828 { 829 function comparatorWrapper(aRow, bRow) 830 { 831 var reverseFactor = this.sortOrder !== WebInspector.DataGrid.SortOrder.Ascending ? -1 : 1; 832 var aNode = aRow._dataGridNode; 833 var bNode = bRow._dataGridNode; 834 if (aNode._data.summaryRow || aNode.isPlaceholderNode) 835 return 1; 836 if (bNode._data.summaryRow || bNode.isPlaceholderNode) 837 return -1; 838 839 return reverseFactor * comparator(aNode, bNode); 840 } 841 842 delete this._sortNodesRequestId; 843 844 if (this._editing) { 845 this._sortAfterEditingCallback = this.sortNodes.bind(this, comparator); 846 return; 847 } 848 849 var tbody = this.dataTableBodyElement; 850 var childNodes = tbody.childNodes; 851 var fillerRowElement = tbody.lastChild; 852 853 var sortedRowElements = Array.prototype.slice.call(childNodes, 0, childNodes.length - 1); 854 sortedRowElements.sort(comparatorWrapper.bind(this)); 855 856 tbody.removeChildren(); 857 858 var previousSiblingNode = null; 859 for (var rowElement of sortedRowElements) { 860 var node = rowElement._dataGridNode; 861 node.previousSibling = previousSiblingNode; 862 if (previousSiblingNode) 863 previousSiblingNode.nextSibling = node; 864 tbody.appendChild(rowElement); 865 previousSiblingNode = node; 866 } 867 868 if (previousSiblingNode) 869 previousSiblingNode.nextSibling = null; 870 871 tbody.appendChild(fillerRowElement); // We expect to find a filler row when attaching nodes. 872 }, 873 874 _keyDown: function(event) 875 { 876 if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing) 877 return; 878 879 var handled = false; 880 var nextSelectedNode; 881 if (event.keyIdentifier === "Up" && !event.altKey) { 882 nextSelectedNode = this.selectedNode.traversePreviousNode(true); 883 while (nextSelectedNode && !nextSelectedNode.selectable) 884 nextSelectedNode = nextSelectedNode.traversePreviousNode(true); 885 handled = nextSelectedNode ? true : false; 886 } else if (event.keyIdentifier === "Down" && !event.altKey) { 887 nextSelectedNode = this.selectedNode.traverseNextNode(true); 888 while (nextSelectedNode && !nextSelectedNode.selectable) 889 nextSelectedNode = nextSelectedNode.traverseNextNode(true); 890 handled = nextSelectedNode ? true : false; 891 } else if (event.keyIdentifier === "Left") { 892 if (this.selectedNode.expanded) { 893 if (event.altKey) 894 this.selectedNode.collapseRecursively(); 895 else 896 this.selectedNode.collapse(); 897 handled = true; 898 } else if (this.selectedNode.parent && !this.selectedNode.parent.root) { 899 handled = true; 900 if (this.selectedNode.parent.selectable) { 901 nextSelectedNode = this.selectedNode.parent; 902 handled = nextSelectedNode ? true : false; 903 } else if (this.selectedNode.parent) 904 this.selectedNode.parent.collapse(); 905 } 906 } else if (event.keyIdentifier === "Right") { 907 if (!this.selectedNode.revealed) { 908 this.selectedNode.reveal(); 909 handled = true; 910 } else if (this.selectedNode.hasChildren) { 911 handled = true; 912 if (this.selectedNode.expanded) { 913 nextSelectedNode = this.selectedNode.children[0]; 914 handled = nextSelectedNode ? true : false; 915 } else { 916 if (event.altKey) 917 this.selectedNode.expandRecursively(); 918 else 919 this.selectedNode.expand(); 920 } 921 } 922 } else if (event.keyCode === 8 || event.keyCode === 46) { 923 if (this._deleteCallback) { 924 handled = true; 925 this._deleteCallback(this.selectedNode); 926 } 927 } else if (isEnterKey(event)) { 928 if (this._editCallback) { 929 handled = true; 930 this._startEditing(this.selectedNode._element.children[0]); 931 } 932 } 933 934 if (nextSelectedNode) { 935 nextSelectedNode.reveal(); 936 nextSelectedNode.select(); 937 } 938 939 if (handled) { 940 event.preventDefault(); 941 event.stopPropagation(); 942 } 943 }, 944 945 expand: function() 946 { 947 // This is the root, do nothing. 948 }, 949 950 collapse: function() 951 { 952 // This is the root, do nothing. 953 }, 954 955 reveal: function() 956 { 957 // This is the root, do nothing. 958 }, 959 960 revealAndSelect: function() 961 { 962 // This is the root, do nothing. 963 }, 964 965 dataGridNodeFromNode: function(target) 966 { 967 var rowElement = target.enclosingNodeOrSelfWithNodeName("tr"); 968 return rowElement && rowElement._dataGridNode; 969 }, 970 971 dataGridNodeFromPoint: function(x, y) 972 { 973 var node = this._dataTableElement.ownerDocument.elementFromPoint(x, y); 974 var rowElement = node.enclosingNodeOrSelfWithNodeName("tr"); 975 return rowElement && rowElement._dataGridNode; 976 }, 977 978 _headerCellClicked: function(event) 979 { 980 var cell = event.target.enclosingNodeOrSelfWithNodeName("th"); 981 if (!cell || !cell.columnIdentifier || !cell.classList.contains(WebInspector.DataGrid.SortableColumnStyleClassName)) 982 return; 983 984 var clickedColumnIdentifier = cell.columnIdentifier; 985 if (this.sortColumnIdentifier === clickedColumnIdentifier) { 986 if (this.sortOrder !== WebInspector.DataGrid.SortOrder.Descending) 987 this.sortOrder = WebInspector.DataGrid.SortOrder.Descending; 988 else 989 this.sortOrder = WebInspector.DataGrid.SortOrder.Ascending; 990 } else 991 this.sortColumnIdentifier = clickedColumnIdentifier; 992 }, 993 994 _mouseoverColumnCollapser: function(event) 995 { 996 var cell = event.target.enclosingNodeOrSelfWithNodeName("th"); 997 if (!cell || !cell.collapsesGroup) 998 return; 999 1000 cell.classList.add("mouse-over-collapser"); 1001 }, 1002 1003 _mouseoutColumnCollapser: function(event) 1004 { 1005 var cell = event.target.enclosingNodeOrSelfWithNodeName("th"); 1006 if (!cell || !cell.collapsesGroup) 1007 return; 1008 1009 cell.classList.remove("mouse-over-collapser"); 1010 }, 1011 1012 _clickInColumnCollapser: function(event) 1013 { 1014 var cell = event.target.enclosingNodeOrSelfWithNodeName("th"); 1015 if (!cell || !cell.collapsesGroup) 1016 return; 1017 1018 this._collapseColumnGroupWithCell(cell); 1019 1020 event.stopPropagation(); 1021 event.preventDefault(); 1022 }, 1023 1024 collapseColumnGroup: function(columnGroup) 1025 { 1026 var collapserColumnIdentifier = null; 1027 for (var [identifier, column] of this.columns) { 1028 if (column["collapsesGroup"] == columnGroup) { 1029 collapserColumnIdentifier = identifier; 1030 break; 1031 } 1032 } 1033 1034 console.assert(collapserColumnIdentifier); 1035 if (!collapserColumnIdentifier) 1036 return; 1037 1038 var cell = this._headerTableCellElements.get(collapserColumnIdentifier); 1039 this._collapseColumnGroupWithCell(cell); 1040 }, 1041 1042 _collapseColumnGroupWithCell: function(cell) 1043 { 1044 var columnsWillCollapse = cell.classList.toggle("collapsed"); 1045 1046 this.willToggleColumnGroup(cell.collapsesGroup, columnsWillCollapse); 1047 1048 var showOrHide = columnsWillCollapse ? this._hideColumn : this._showColumn; 1049 for (var [identifier, column] of this.columns) { 1050 if (column["group"] === cell.collapsesGroup) 1051 showOrHide.call(this, identifier); 1052 } 1053 1054 var collapserButton = cell.querySelector(".collapser-button"); 1055 if (collapserButton) 1056 collapserButton.title = columnsWillCollapse ? this._collapserButtonExpandColumnsToolTip() : this._collapserButtonCollapseColumnsToolTip(); 1057 1058 this.didToggleColumnGroup(cell.collapsesGroup, columnsWillCollapse); 1059 }, 1060 1061 _collapserButtonCollapseColumnsToolTip: function() 1062 { 1063 return WebInspector.UIString("Collapse columns"); 1064 }, 1065 1066 _collapserButtonExpandColumnsToolTip: function() 1067 { 1068 return WebInspector.UIString("Expand columns"); 1069 }, 1070 1071 willToggleColumnGroup: function(columnGroup, willCollapse) 1072 { 1073 // Implemented by subclasses if needed. 1074 }, 1075 1076 didToggleColumnGroup: function(columnGroup, didCollapse) 1077 { 1078 // Implemented by subclasses if needed. 1079 }, 1080 1081 headerTableHeader: function(columnIdentifier) 1082 { 1083 return this._headerTableCellElements.get(columnIdentifier); 1084 }, 1085 1086 _generateSortIndicatorImagesIfNeeded: function() 1087 { 1088 if (WebInspector.DataGrid._generatedSortIndicatorImages) 1089 return; 1090 1091 WebInspector.DataGrid._generatedSortIndicatorImages = true; 1092 1093 var specifications = {}; 1094 1095 if (WebInspector.Platform.isLegacyMacOS) { 1096 specifications["arrow"] = { 1097 fillColor: [81, 81, 81], 1098 shadowColor: [255, 255, 255, 0.5], 1099 shadowOffsetX: 0, 1100 shadowOffsetY: 1, 1101 shadowBlur: 0 1102 }; 1103 } else { 1104 specifications["arrow"] = { 1105 fillColor: [81, 81, 81], 1106 }; 1107 } 1108 1109 generateColoredImagesForCSS(platformImagePath("SortIndicatorDownArrow.svg"), specifications, 9, 8, "data-grid-sort-indicator-down-"); 1110 generateColoredImagesForCSS(platformImagePath("SortIndicatorUpArrow.svg"), specifications, 9, 8, "data-grid-sort-indicator-up-"); 1111 }, 1112 1113 _mouseDownInDataTable: function(event) 1114 { 1115 var gridNode = this.dataGridNodeFromNode(event.target); 1116 if (!gridNode || !gridNode.selectable) 1117 return; 1118 1119 if (gridNode.isEventWithinDisclosureTriangle(event)) 1120 return; 1121 1122 if (event.metaKey) { 1123 if (gridNode.selected) 1124 gridNode.deselect(); 1125 else 1126 gridNode.select(); 1127 } else 1128 gridNode.select(); 1129 }, 1130 1131 _contextMenuInDataTable: function(event) 1132 { 1133 var contextMenu = new WebInspector.ContextMenu(event); 1134 1135 var gridNode = this.dataGridNodeFromNode(event.target); 1136 if (this.dataGrid._refreshCallback && (!gridNode || gridNode !== this.placeholderNode)) 1137 contextMenu.appendItem(WebInspector.UIString("Refresh"), this._refreshCallback.bind(this)); 1138 1139 if (gridNode && gridNode.selectable && !gridNode.isEventWithinDisclosureTriangle(event)) { 1140 contextMenu.appendItem(WebInspector.UIString("Copy Row"), this._copyRow.bind(this, event.target)); 1141 1142 if (this.dataGrid._editCallback) { 1143 if (gridNode === this.placeholderNode) 1144 contextMenu.appendItem(WebInspector.UIString("Add New"), this._startEditing.bind(this, event.target)); 1145 else { 1146 var element = event.target.enclosingNodeOrSelfWithNodeName("td"); 1147 var columnIdentifier = element.__columnIdentifier; 1148 var columnTitle = this.dataGrid.columns.get(columnIdentifier).get("title"); 1149 contextMenu.appendItem(WebInspector.UIString("Edit “%s”").format(columnTitle), this._startEditing.bind(this, event.target)); 1150 } 1151 } 1152 if (this.dataGrid._deleteCallback && gridNode !== this.placeholderNode) 1153 contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode)); 1154 } 1155 1156 contextMenu.show(); 1157 }, 1158 1159 _clickInDataTable: function(event) 1160 { 1161 var gridNode = this.dataGridNodeFromNode(event.target); 1162 if (!gridNode || !gridNode.hasChildren) 1163 return; 1164 1165 if (!gridNode.isEventWithinDisclosureTriangle(event)) 1166 return; 1167 1168 if (gridNode.expanded) { 1169 if (event.altKey) 1170 gridNode.collapseRecursively(); 1171 else 1172 gridNode.collapse(); 1173 } else { 1174 if (event.altKey) 1175 gridNode.expandRecursively(); 1176 else 1177 gridNode.expand(); 1178 } 1179 }, 1180 1181 _copyTextForDataGridNode: function(node) 1182 { 1183 var fields = []; 1184 for (var identifier of node.dataGrid.orderedColumns) 1185 fields.push(node.data[identifier] || ""); 1186 1187 var tabSeparatedValues = fields.join("\t"); 1188 return tabSeparatedValues; 1189 }, 1190 1191 handleBeforeCopyEvent: function(event) 1192 { 1193 if (this.selectedNode && window.getSelection().isCollapsed) 1194 event.preventDefault(); 1195 }, 1196 1197 handleCopyEvent: function(event) 1198 { 1199 if (!this.selectedNode || !window.getSelection().isCollapsed) 1200 return; 1201 1202 var copyText = this._copyTextForDataGridNode(this.selectedNode); 1203 event.clipboardData.setData("text/plain", copyText); 1204 event.stopPropagation(); 1205 event.preventDefault(); 1206 }, 1207 1208 _copyRow: function(target) 1209 { 1210 var gridNode = this.dataGridNodeFromNode(target); 1211 if (!gridNode) 1212 return; 1213 1214 var copyText = this._copyTextForDataGridNode(gridNode); 1215 InspectorFrontendHost.copyText(copyText); 1216 }, 1217 1218 get resizeMethod() 1219 { 1220 if (typeof this._resizeMethod === "undefined") 1221 return WebInspector.DataGrid.ResizeMethod.Nearest; 1222 return this._resizeMethod; 1223 }, 1224 1225 set resizeMethod(method) 1226 { 1227 this._resizeMethod = method; 1228 }, 1229 1230 _startResizerDragging: function(event) 1231 { 1232 if (event.button !== 0 || event.ctrlKey) 1233 return; 1234 1235 this._currentResizer = event.target; 1236 if (!this._currentResizer.rightNeighboringColumnID) 1237 return; 1238 1239 WebInspector.elementDragStart(this._currentResizer, this._resizerDragging.bind(this), 1240 this._endResizerDragging.bind(this), event, "col-resize"); 1241 }, 1242 1243 _resizerDragging: function(event) 1244 { 1245 if (event.button !== 0) 1246 return; 1247 1248 var resizer = this._currentResizer; 1249 if (!resizer) 1250 return; 1251 1252 // Constrain the dragpoint to be within the containing div of the 1253 // datagrid. 1254 var dragPoint = event.clientX - this.element.totalOffsetLeft; 1255 // Constrain the dragpoint to be within the space made up by the 1256 // column directly to the left and the column directly to the right. 1257 var leftCellIndex = resizer.leftNeighboringColumnID; 1258 var rightCellIndex = resizer.rightNeighboringColumnID; 1259 var firstRowCells = this._headerTableBodyElement.rows[0].cells; 1260 var leftEdgeOfPreviousColumn = 0; 1261 for (var i = 0; i < leftCellIndex; i++) 1262 leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth; 1263 1264 // Differences for other resize methods 1265 if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.Last) { 1266 rightCellIndex = this.resizerElements.length; 1267 } else if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.First) { 1268 leftEdgeOfPreviousColumn += firstRowCells[leftCellIndex].offsetWidth - firstRowCells[0].offsetWidth; 1269 leftCellIndex = 0; 1270 } 1271 1272 var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[leftCellIndex].offsetWidth + firstRowCells[rightCellIndex].offsetWidth; 1273 1274 // Give each column some padding so that they don't disappear. 1275 var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding; 1276 var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding; 1277 1278 dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum); 1279 1280 resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px"; 1281 1282 var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / this._dataTableElement.offsetWidth) * 100) + "%"; 1283 this._headerTableColumnGroupElement.children[leftCellIndex].style.width = percentLeftColumn; 1284 this._dataTableColumnGroupElement.children[leftCellIndex].style.width = percentLeftColumn; 1285 1286 var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / this._dataTableElement.offsetWidth) * 100) + "%"; 1287 this._headerTableColumnGroupElement.children[rightCellIndex].style.width = percentRightColumn; 1288 this._dataTableColumnGroupElement.children[rightCellIndex].style.width = percentRightColumn; 1289 1290 this._positionResizerElements(); 1291 event.preventDefault(); 1292 this.dispatchEventToListeners(WebInspector.DataGrid.Event.DidLayout); 1293 }, 1294 1295 _endResizerDragging: function(event) 1296 { 1297 if (event.button !== 0) 1298 return; 1299 1300 WebInspector.elementDragEnd(event); 1301 this._currentResizer = null; 1302 this.dispatchEventToListeners(WebInspector.DataGrid.Event.DidLayout); 1303 }, 1304 1305 ColumnResizePadding: 10, 1306 1307 CenterResizerOverBorderAdjustment: 3, 1308} 1309 1310WebInspector.DataGrid.ResizeMethod = { 1311 Nearest: "nearest", 1312 First: "first", 1313 Last: "last" 1314}; 1315 1316WebInspector.DataGrid.prototype.__proto__ = WebInspector.Object.prototype; 1317 1318WebInspector.DataGridNode = function(data, hasChildren) 1319{ 1320 this._expanded = false; 1321 this._selected = false; 1322 this._shouldRefreshChildren = true; 1323 this._data = data || {}; 1324 this.hasChildren = hasChildren || false; 1325 this.children = []; 1326 this.dataGrid = null; 1327 this.parent = null; 1328 this.previousSibling = null; 1329 this.nextSibling = null; 1330 this.disclosureToggleWidth = 10; 1331} 1332 1333WebInspector.DataGridNode.prototype = { 1334 get selectable() 1335 { 1336 return !this._element || !this._element.classList.contains("hidden"); 1337 }, 1338 1339 get element() 1340 { 1341 if (this._element) 1342 return this._element; 1343 1344 if (!this.dataGrid) 1345 return null; 1346 1347 this._element = document.createElement("tr"); 1348 this._element._dataGridNode = this; 1349 1350 if (this.hasChildren) 1351 this._element.classList.add("parent"); 1352 if (this.expanded) 1353 this._element.classList.add("expanded"); 1354 if (this.selected) 1355 this._element.classList.add("selected"); 1356 if (this.revealed) 1357 this._element.classList.add("revealed"); 1358 1359 this.createCells(); 1360 return this._element; 1361 }, 1362 1363 createCells: function() 1364 { 1365 for (var columnIdentifier of this.dataGrid.orderedColumns) 1366 this._element.appendChild(this.createCell(columnIdentifier)); 1367 }, 1368 1369 refreshIfNeeded: function() 1370 { 1371 if (!this._needsRefresh) 1372 return; 1373 1374 delete this._needsRefresh; 1375 1376 this.refresh(); 1377 }, 1378 1379 needsRefresh: function() 1380 { 1381 this._needsRefresh = true; 1382 1383 if (!this._revealed) 1384 return; 1385 1386 if (this._scheduledRefreshIdentifier) 1387 return; 1388 1389 this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this)); 1390 }, 1391 1392 get data() 1393 { 1394 return this._data; 1395 }, 1396 1397 set data(x) 1398 { 1399 this._data = x || {}; 1400 this.needsRefresh(); 1401 }, 1402 1403 get revealed() 1404 { 1405 if ("_revealed" in this) 1406 return this._revealed; 1407 1408 var currentAncestor = this.parent; 1409 while (currentAncestor && !currentAncestor.root) { 1410 if (!currentAncestor.expanded) { 1411 this._revealed = false; 1412 return false; 1413 } 1414 1415 currentAncestor = currentAncestor.parent; 1416 } 1417 1418 this._revealed = true; 1419 return true; 1420 }, 1421 1422 set hasChildren(x) 1423 { 1424 if (this._hasChildren === x) 1425 return; 1426 1427 this._hasChildren = x; 1428 1429 if (!this._element) 1430 return; 1431 1432 if (this._hasChildren) 1433 { 1434 this._element.classList.add("parent"); 1435 if (this.expanded) 1436 this._element.classList.add("expanded"); 1437 } 1438 else 1439 { 1440 this._element.classList.remove("parent"); 1441 this._element.classList.remove("expanded"); 1442 } 1443 }, 1444 1445 get hasChildren() 1446 { 1447 return this._hasChildren; 1448 }, 1449 1450 set revealed(x) 1451 { 1452 if (this._revealed === x) 1453 return; 1454 1455 this._revealed = x; 1456 1457 if (this._element) { 1458 if (this._revealed) 1459 this._element.classList.add("revealed"); 1460 else 1461 this._element.classList.remove("revealed"); 1462 } 1463 1464 this.refreshIfNeeded(); 1465 1466 for (var i = 0; i < this.children.length; ++i) 1467 this.children[i].revealed = x && this.expanded; 1468 }, 1469 1470 get depth() 1471 { 1472 if ("_depth" in this) 1473 return this._depth; 1474 if (this.parent && !this.parent.root) 1475 this._depth = this.parent.depth + 1; 1476 else 1477 this._depth = 0; 1478 return this._depth; 1479 }, 1480 1481 get leftPadding() 1482 { 1483 if (typeof(this._leftPadding) === "number") 1484 return this._leftPadding; 1485 1486 this._leftPadding = this.depth * this.dataGrid.indentWidth; 1487 return this._leftPadding; 1488 }, 1489 1490 get shouldRefreshChildren() 1491 { 1492 return this._shouldRefreshChildren; 1493 }, 1494 1495 set shouldRefreshChildren(x) 1496 { 1497 this._shouldRefreshChildren = x; 1498 if (x && this.expanded) 1499 this.expand(); 1500 }, 1501 1502 get selected() 1503 { 1504 return this._selected; 1505 }, 1506 1507 set selected(x) 1508 { 1509 if (x) 1510 this.select(); 1511 else 1512 this.deselect(); 1513 }, 1514 1515 get expanded() 1516 { 1517 return this._expanded; 1518 }, 1519 1520 set expanded(x) 1521 { 1522 if (x) 1523 this.expand(); 1524 else 1525 this.collapse(); 1526 }, 1527 1528 refresh: function() 1529 { 1530 if (!this._element || !this.dataGrid) 1531 return; 1532 1533 if (this._scheduledRefreshIdentifier) { 1534 cancelAnimationFrame(this._scheduledRefreshIdentifier); 1535 delete this._scheduledRefreshIdentifier; 1536 } 1537 1538 delete this._needsRefresh; 1539 1540 this._element.removeChildren(); 1541 this.createCells(); 1542 }, 1543 1544 updateLayout: function() 1545 { 1546 // Implemented by subclasses if needed. 1547 }, 1548 1549 createCell: function(columnIdentifier) 1550 { 1551 var cellElement = document.createElement("td"); 1552 cellElement.className = columnIdentifier + "-column"; 1553 cellElement.__columnIdentifier = columnIdentifier; 1554 1555 var column = this.dataGrid.columns.get(columnIdentifier); 1556 1557 if (column["aligned"]) 1558 cellElement.classList.add(column["aligned"]); 1559 1560 if (column["group"]) 1561 cellElement.classList.add("column-group-" + column["group"]); 1562 1563 var div = cellElement.createChild("div"); 1564 var content = this.createCellContent(columnIdentifier, cellElement); 1565 div.appendChild(content instanceof Node ? content : document.createTextNode(content)); 1566 1567 if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) { 1568 cellElement.classList.add("disclosure"); 1569 if (this.leftPadding) 1570 cellElement.style.setProperty("padding-left", this.leftPadding + "px"); 1571 } 1572 1573 return cellElement; 1574 }, 1575 1576 createCellContent: function(columnIdentifier) 1577 { 1578 return this.data[columnIdentifier] || "\u200b"; // Zero width space to keep the cell from collapsing. 1579 }, 1580 1581 elementWithColumnIdentifier: function(columnIdentifier) 1582 { 1583 var index = this.dataGrid.orderedColumns.indexOf(columnIdentifier); 1584 if (index === -1) 1585 return null; 1586 1587 return this._element.children[index]; 1588 }, 1589 1590 // Share these functions with DataGrid. They are written to work with a DataGridNode this object. 1591 appendChild: WebInspector.DataGrid.prototype.appendChild, 1592 insertChild: WebInspector.DataGrid.prototype.insertChild, 1593 removeChild: WebInspector.DataGrid.prototype.removeChild, 1594 removeChildren: WebInspector.DataGrid.prototype.removeChildren, 1595 removeChildrenRecursive: WebInspector.DataGrid.prototype.removeChildrenRecursive, 1596 1597 _recalculateSiblings: function(myIndex) 1598 { 1599 if (!this.parent) 1600 return; 1601 1602 var previousChild = (myIndex > 0 ? this.parent.children[myIndex - 1] : null); 1603 1604 if (previousChild) { 1605 previousChild.nextSibling = this; 1606 this.previousSibling = previousChild; 1607 } else 1608 this.previousSibling = null; 1609 1610 var nextChild = this.parent.children[myIndex + 1]; 1611 1612 if (nextChild) { 1613 nextChild.previousSibling = this; 1614 this.nextSibling = nextChild; 1615 } else 1616 this.nextSibling = null; 1617 }, 1618 1619 collapse: function() 1620 { 1621 if (this._element) 1622 this._element.classList.remove("expanded"); 1623 1624 this._expanded = false; 1625 1626 for (var i = 0; i < this.children.length; ++i) 1627 this.children[i].revealed = false; 1628 1629 this.dispatchEventToListeners("collapsed"); 1630 1631 if (this.dataGrid) 1632 this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.CollapsedNode, {dataGridNode: this}); 1633 }, 1634 1635 collapseRecursively: function() 1636 { 1637 var item = this; 1638 while (item) { 1639 if (item.expanded) 1640 item.collapse(); 1641 item = item.traverseNextNode(false, this, true); 1642 } 1643 }, 1644 1645 expand: function() 1646 { 1647 if (!this.hasChildren || this.expanded) 1648 return; 1649 1650 if (this.revealed && !this._shouldRefreshChildren) 1651 for (var i = 0; i < this.children.length; ++i) 1652 this.children[i].revealed = true; 1653 1654 if (this._shouldRefreshChildren) { 1655 for (var i = 0; i < this.children.length; ++i) 1656 this.children[i]._detach(); 1657 1658 this.dispatchEventToListeners("populate"); 1659 1660 if (this._attached) { 1661 for (var i = 0; i < this.children.length; ++i) { 1662 var child = this.children[i]; 1663 if (this.revealed) 1664 child.revealed = true; 1665 child._attach(); 1666 } 1667 } 1668 1669 delete this._shouldRefreshChildren; 1670 } 1671 1672 if (this._element) 1673 this._element.classList.add("expanded"); 1674 1675 this._expanded = true; 1676 1677 this.dispatchEventToListeners("expanded"); 1678 1679 if (this.dataGrid) 1680 this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.ExpandedNode, {dataGridNode: this}); 1681 }, 1682 1683 expandRecursively: function() 1684 { 1685 var item = this; 1686 while (item) { 1687 item.expand(); 1688 item = item.traverseNextNode(false, this); 1689 } 1690 }, 1691 1692 reveal: function() 1693 { 1694 var currentAncestor = this.parent; 1695 while (currentAncestor && !currentAncestor.root) { 1696 if (!currentAncestor.expanded) 1697 currentAncestor.expand(); 1698 currentAncestor = currentAncestor.parent; 1699 } 1700 1701 this.element.scrollIntoViewIfNeeded(false); 1702 1703 this.dispatchEventToListeners("revealed"); 1704 }, 1705 1706 select: function(supressSelectedEvent) 1707 { 1708 if (!this.dataGrid || !this.selectable || this.selected) 1709 return; 1710 1711 if (this.dataGrid.selectedNode) 1712 this.dataGrid.selectedNode.deselect(); 1713 1714 this._selected = true; 1715 this.dataGrid.selectedNode = this; 1716 1717 if (this._element) 1718 this._element.classList.add("selected"); 1719 1720 if (!supressSelectedEvent) 1721 this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.SelectedNodeChanged); 1722 }, 1723 1724 revealAndSelect: function() 1725 { 1726 this.reveal(); 1727 this.select(); 1728 }, 1729 1730 deselect: function(supressDeselectedEvent) 1731 { 1732 if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected) 1733 return; 1734 1735 this._selected = false; 1736 this.dataGrid.selectedNode = null; 1737 1738 if (this._element) 1739 this._element.classList.remove("selected"); 1740 1741 if (!supressDeselectedEvent) 1742 this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.SelectedNodeChanged); 1743 }, 1744 1745 traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info) 1746 { 1747 if (!dontPopulate && this.hasChildren) 1748 this.dispatchEventToListeners("populate"); 1749 1750 if (info) 1751 info.depthChange = 0; 1752 1753 var node = (!skipHidden || this.revealed) ? this.children[0] : null; 1754 if (node && (!skipHidden || this.expanded)) { 1755 if (info) 1756 info.depthChange = 1; 1757 return node; 1758 } 1759 1760 if (this === stayWithin) 1761 return null; 1762 1763 node = (!skipHidden || this.revealed) ? this.nextSibling : null; 1764 if (node) 1765 return node; 1766 1767 node = this; 1768 while (node && !node.root && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) { 1769 if (info) 1770 info.depthChange -= 1; 1771 node = node.parent; 1772 } 1773 1774 if (!node) 1775 return null; 1776 1777 return (!skipHidden || node.revealed) ? node.nextSibling : null; 1778 }, 1779 1780 traversePreviousNode: function(skipHidden, dontPopulate) 1781 { 1782 var node = (!skipHidden || this.revealed) ? this.previousSibling : null; 1783 if (!dontPopulate && node && node.hasChildren) 1784 node.dispatchEventToListeners("populate"); 1785 1786 while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children.lastValue : null)) { 1787 if (!dontPopulate && node.hasChildren) 1788 node.dispatchEventToListeners("populate"); 1789 node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children.lastValue : null); 1790 } 1791 1792 if (node) 1793 return node; 1794 1795 if (!this.parent || this.parent.root) 1796 return null; 1797 1798 return this.parent; 1799 }, 1800 1801 isEventWithinDisclosureTriangle: function(event) 1802 { 1803 if (!this.hasChildren) 1804 return false; 1805 var cell = event.target.enclosingNodeOrSelfWithNodeName("td"); 1806 if (!cell.classList.contains("disclosure")) 1807 return false; 1808 1809 var left = cell.totalOffsetLeft + this.leftPadding; 1810 return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth; 1811 }, 1812 1813 _attach: function() 1814 { 1815 if (!this.dataGrid || this._attached) 1816 return; 1817 1818 this._attached = true; 1819 1820 var nextElement = null; 1821 1822 var previousGridNode = this.traversePreviousNode(true, true); 1823 if (previousGridNode && previousGridNode.element.parentNode) 1824 nextElement = previousGridNode.element.nextSibling; 1825 else if (!previousGridNode) 1826 nextElement = this.dataGrid.dataTableBodyElement.firstChild; 1827 1828 // If there is no next grid node, then append before the last child since the last child is the filler row. 1829 console.assert(this.dataGrid.dataTableBodyElement.lastChild.classList.contains("filler")); 1830 1831 if (!nextElement) 1832 nextElement = this.dataGrid.dataTableBodyElement.lastChild; 1833 1834 this.dataGrid.dataTableBodyElement.insertBefore(this.element, nextElement); 1835 1836 if (this.expanded) 1837 for (var i = 0; i < this.children.length; ++i) 1838 this.children[i]._attach(); 1839 }, 1840 1841 _detach: function() 1842 { 1843 if (!this._attached) 1844 return; 1845 1846 this._attached = false; 1847 1848 if (this._element && this._element.parentNode) 1849 this._element.parentNode.removeChild(this._element); 1850 1851 for (var i = 0; i < this.children.length; ++i) 1852 this.children[i]._detach(); 1853 }, 1854 1855 savePosition: function() 1856 { 1857 if (this._savedPosition) 1858 return; 1859 1860 if (!this.parent) 1861 throw("savePosition: Node must have a parent."); 1862 this._savedPosition = { 1863 parent: this.parent, 1864 index: this.parent.children.indexOf(this) 1865 }; 1866 }, 1867 1868 restorePosition: function() 1869 { 1870 if (!this._savedPosition) 1871 return; 1872 1873 if (this.parent !== this._savedPosition.parent) 1874 this._savedPosition.parent.insertChild(this, this._savedPosition.index); 1875 1876 delete this._savedPosition; 1877 } 1878} 1879 1880WebInspector.DataGridNode.prototype.__proto__ = WebInspector.Object.prototype; 1881 1882// Used to create a new table row when entering new data by editing cells. 1883WebInspector.PlaceholderDataGridNode = function(data) 1884{ 1885 WebInspector.DataGridNode.call(this, data, false); 1886 this.isPlaceholderNode = true; 1887} 1888 1889WebInspector.PlaceholderDataGridNode.prototype = { 1890 constructor: WebInspector.PlaceholderDataGridNode, 1891 __proto__: WebInspector.DataGridNode.prototype, 1892 1893 makeNormal: function() 1894 { 1895 delete this.isPlaceholderNode; 1896 delete this.makeNormal; 1897 } 1898} 1899