1/* 2 * Copyright (C) 2008 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(columns, editCallback, deleteCallback) 27{ 28 this.element = document.createElement("div"); 29 this.element.className = "data-grid"; 30 this.element.tabIndex = 0; 31 this.element.addEventListener("keydown", this._keyDown.bind(this), false); 32 33 this._headerTable = document.createElement("table"); 34 this._headerTable.className = "header"; 35 this._headerTableHeaders = {}; 36 37 this._dataTable = document.createElement("table"); 38 this._dataTable.className = "data"; 39 40 this._dataTable.addEventListener("mousedown", this._mouseDownInDataTable.bind(this), true); 41 this._dataTable.addEventListener("click", this._clickInDataTable.bind(this), true); 42 43 this._dataTable.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true); 44 45 // FIXME: Add a createCallback which is different from editCallback and has different 46 // behavior when creating a new node. 47 if (editCallback) { 48 this._dataTable.addEventListener("dblclick", this._ondblclick.bind(this), false); 49 this._editCallback = editCallback; 50 } 51 if (deleteCallback) 52 this._deleteCallback = deleteCallback; 53 54 this.aligned = {}; 55 56 this._scrollContainer = document.createElement("div"); 57 this._scrollContainer.className = "data-container"; 58 this._scrollContainer.appendChild(this._dataTable); 59 60 this.element.appendChild(this._headerTable); 61 this.element.appendChild(this._scrollContainer); 62 63 var headerRow = document.createElement("tr"); 64 var columnGroup = document.createElement("colgroup"); 65 this._columnCount = 0; 66 67 for (var columnIdentifier in columns) { 68 var column = columns[columnIdentifier]; 69 if (column.disclosure) 70 this.disclosureColumnIdentifier = columnIdentifier; 71 72 var col = document.createElement("col"); 73 if (column.width) 74 col.style.width = column.width; 75 column.element = col; 76 columnGroup.appendChild(col); 77 78 var cell = document.createElement("th"); 79 cell.className = columnIdentifier + "-column"; 80 cell.columnIdentifier = columnIdentifier; 81 this._headerTableHeaders[columnIdentifier] = cell; 82 83 var div = document.createElement("div"); 84 if (column.titleDOMFragment) 85 div.appendChild(column.titleDOMFragment); 86 else 87 div.textContent = column.title; 88 cell.appendChild(div); 89 90 if (column.sort) { 91 cell.addStyleClass("sort-" + column.sort); 92 this._sortColumnCell = cell; 93 } 94 95 if (column.sortable) { 96 cell.addEventListener("click", this._clickInHeaderCell.bind(this), false); 97 cell.addStyleClass("sortable"); 98 } 99 100 if (column.aligned) 101 this.aligned[columnIdentifier] = column.aligned; 102 103 headerRow.appendChild(cell); 104 105 ++this._columnCount; 106 } 107 108 columnGroup.span = this._columnCount; 109 110 var cell = document.createElement("th"); 111 cell.className = "corner"; 112 headerRow.appendChild(cell); 113 114 this._headerTableColumnGroup = columnGroup; 115 this._headerTable.appendChild(this._headerTableColumnGroup); 116 this.headerTableBody.appendChild(headerRow); 117 118 var fillerRow = document.createElement("tr"); 119 fillerRow.className = "filler"; 120 121 for (var columnIdentifier in columns) { 122 var column = columns[columnIdentifier]; 123 var cell = document.createElement("td"); 124 cell.className = columnIdentifier + "-column"; 125 fillerRow.appendChild(cell); 126 } 127 128 this._dataTableColumnGroup = columnGroup.cloneNode(true); 129 this._dataTable.appendChild(this._dataTableColumnGroup); 130 this.dataTableBody.appendChild(fillerRow); 131 132 this.columns = columns || {}; 133 this._columnsArray = []; 134 for (var columnIdentifier in columns) { 135 columns[columnIdentifier].ordinal = this._columnsArray.length; 136 columns[columnIdentifier].identifier = columnIdentifier; 137 this._columnsArray.push(columns[columnIdentifier]); 138 } 139 140 for (var i = 0; i < this._columnsArray.length; ++i) 141 this._columnsArray[i].bodyElement = this._dataTableColumnGroup.children[i]; 142 143 this.children = []; 144 this.selectedNode = null; 145 this.expandNodesWhenArrowing = false; 146 this.root = true; 147 this.hasChildren = false; 148 this.expanded = true; 149 this.revealed = true; 150 this.selected = false; 151 this.dataGrid = this; 152 this.indentWidth = 15; 153 this.resizers = []; 154 this._columnWidthsInitialized = false; 155} 156 157WebInspector.DataGrid.prototype = { 158 _ondblclick: function(event) 159 { 160 if (this._editing || this._editingNode) 161 return; 162 163 this._startEditing(event.target); 164 }, 165 166 _startEditingColumnOfDataGridNode: function(node, column) 167 { 168 this._editing = true; 169 this._editingNode = node; 170 this._editingNode.select(); 171 172 var element = this._editingNode._element.children[column]; 173 WebInspector.startEditing(element, { 174 context: element.textContent, 175 commitHandler: this._editingCommitted.bind(this), 176 cancelHandler: this._editingCancelled.bind(this) 177 }); 178 window.getSelection().setBaseAndExtent(element, 0, element, 1); 179 }, 180 181 _startEditing: function(target) 182 { 183 var element = target.enclosingNodeOrSelfWithNodeName("td"); 184 if (!element) 185 return; 186 187 this._editingNode = this.dataGridNodeFromNode(target); 188 if (!this._editingNode) { 189 if (!this.creationNode) 190 return; 191 this._editingNode = this.creationNode; 192 } 193 194 // Force editing the 1st column when editing the creation node 195 if (this._editingNode.isCreationNode) 196 return this._startEditingColumnOfDataGridNode(this._editingNode, 0); 197 198 this._editing = true; 199 WebInspector.startEditing(element, { 200 context: element.textContent, 201 commitHandler: this._editingCommitted.bind(this), 202 cancelHandler: this._editingCancelled.bind(this) 203 }); 204 window.getSelection().setBaseAndExtent(element, 0, element, 1); 205 }, 206 207 _editingCommitted: function(element, newText, oldText, context, moveDirection) 208 { 209 // FIXME: We need more column identifiers here throughout this function. 210 // Not needed yet since only editable DataGrid is DOM Storage, which is Key - Value. 211 212 // FIXME: Better way to do this than regular expressions? 213 var columnIdentifier = parseInt(element.className.match(/\b(\d+)-column\b/)[1]); 214 215 var textBeforeEditing = this._editingNode.data[columnIdentifier]; 216 var currentEditingNode = this._editingNode; 217 218 function moveToNextIfNeeded(wasChange) { 219 if (!moveDirection) 220 return; 221 222 if (moveDirection === "forward") { 223 if (currentEditingNode.isCreationNode && columnIdentifier === 0 && !wasChange) 224 return; 225 226 if (columnIdentifier === 0) 227 return this._startEditingColumnOfDataGridNode(currentEditingNode, 1); 228 229 var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true); 230 if (nextDataGridNode) 231 return this._startEditingColumnOfDataGridNode(nextDataGridNode, 0); 232 if (currentEditingNode.isCreationNode && wasChange) { 233 addCreationNode(false); 234 return this._startEditingColumnOfDataGridNode(this.creationNode, 0); 235 } 236 return; 237 } 238 239 if (moveDirection === "backward") { 240 if (columnIdentifier === 1) 241 return this._startEditingColumnOfDataGridNode(currentEditingNode, 0); 242 var nextDataGridNode = currentEditingNode.traversePreviousNode(true, null, true); 243 244 if (nextDataGridNode) 245 return this._startEditingColumnOfDataGridNode(nextDataGridNode, 1); 246 return; 247 } 248 } 249 250 if (textBeforeEditing == newText) { 251 this._editingCancelled(element); 252 moveToNextIfNeeded.call(this, false); 253 return; 254 } 255 256 // Update the text in the datagrid that we typed 257 this._editingNode.data[columnIdentifier] = newText; 258 259 // Make the callback - expects an editing node (table row), the column number that is being edited, 260 // the text that used to be there, and the new text. 261 this._editCallback(this._editingNode, columnIdentifier, textBeforeEditing, newText); 262 263 if (this._editingNode.isCreationNode) 264 this.addCreationNode(false); 265 266 this._editingCancelled(element); 267 moveToNextIfNeeded.call(this, true); 268 }, 269 270 _editingCancelled: function(element, context) 271 { 272 delete this._editing; 273 this._editingNode = null; 274 }, 275 276 get sortColumnIdentifier() 277 { 278 if (!this._sortColumnCell) 279 return null; 280 return this._sortColumnCell.columnIdentifier; 281 }, 282 283 get sortOrder() 284 { 285 if (!this._sortColumnCell || this._sortColumnCell.hasStyleClass("sort-ascending")) 286 return "ascending"; 287 if (this._sortColumnCell.hasStyleClass("sort-descending")) 288 return "descending"; 289 return null; 290 }, 291 292 get headerTableBody() 293 { 294 if ("_headerTableBody" in this) 295 return this._headerTableBody; 296 297 this._headerTableBody = this._headerTable.getElementsByTagName("tbody")[0]; 298 if (!this._headerTableBody) { 299 this._headerTableBody = this.element.ownerDocument.createElement("tbody"); 300 this._headerTable.insertBefore(this._headerTableBody, this._headerTable.tFoot); 301 } 302 303 return this._headerTableBody; 304 }, 305 306 get dataTableBody() 307 { 308 if ("_dataTableBody" in this) 309 return this._dataTableBody; 310 311 this._dataTableBody = this._dataTable.getElementsByTagName("tbody")[0]; 312 if (!this._dataTableBody) { 313 this._dataTableBody = this.element.ownerDocument.createElement("tbody"); 314 this._dataTable.insertBefore(this._dataTableBody, this._dataTable.tFoot); 315 } 316 317 return this._dataTableBody; 318 }, 319 320 autoSizeColumns: function(minPercent, maxPercent, maxDescentLevel) 321 { 322 if (minPercent) 323 minPercent = Math.min(minPercent, Math.floor(100 / this._columnCount)); 324 var widths = {}; 325 var columns = this.columns; 326 for (var columnIdentifier in columns) 327 widths[columnIdentifier] = (columns[columnIdentifier].title || "").length; 328 329 var children = maxDescentLevel ? this._enumerateChildren(this, [], maxDescentLevel + 1) : this.children; 330 for (var i = 0; i < children.length; ++i) { 331 var node = children[i]; 332 for (var columnIdentifier in columns) { 333 var text = node.data[columnIdentifier] || ""; 334 if (text.length > widths[columnIdentifier]) 335 widths[columnIdentifier] = text.length; 336 } 337 } 338 339 var totalColumnWidths = 0; 340 for (var columnIdentifier in columns) 341 totalColumnWidths += widths[columnIdentifier]; 342 343 var recoupPercent = 0; 344 for (var columnIdentifier in columns) { 345 var width = Math.round(100 * widths[columnIdentifier] / totalColumnWidths); 346 if (minPercent && width < minPercent) { 347 recoupPercent += (minPercent - width); 348 width = minPercent; 349 } else if (maxPercent && width > maxPercent) { 350 recoupPercent -= (width - maxPercent); 351 width = maxPercent; 352 } 353 widths[columnIdentifier] = width; 354 } 355 356 while (minPercent && recoupPercent > 0) { 357 for (var columnIdentifier in columns) { 358 if (widths[columnIdentifier] > minPercent) { 359 --widths[columnIdentifier]; 360 --recoupPercent; 361 if (!recoupPercent) 362 break; 363 } 364 } 365 } 366 367 while (maxPercent && recoupPercent < 0) { 368 for (var columnIdentifier in columns) { 369 if (widths[columnIdentifier] < maxPercent) { 370 ++widths[columnIdentifier]; 371 ++recoupPercent; 372 if (!recoupPercent) 373 break; 374 } 375 } 376 } 377 378 for (var columnIdentifier in columns) 379 columns[columnIdentifier].element.style.width = widths[columnIdentifier] + "%"; 380 this._columnWidthsInitialized = false; 381 this.updateWidths(); 382 }, 383 384 _enumerateChildren: function(rootNode, result, maxLevel) 385 { 386 if (!rootNode.root) 387 result.push(rootNode); 388 if (!maxLevel) 389 return; 390 for (var i = 0; i < rootNode.children.length; ++i) 391 this._enumerateChildren(rootNode.children[i], result, maxLevel - 1); 392 return result; 393 }, 394 395 // Updates the widths of the table, including the positions of the column 396 // resizers. 397 // 398 // IMPORTANT: This function MUST be called once after the element of the 399 // DataGrid is attached to its parent element and every subsequent time the 400 // width of the parent element is changed in order to make it possible to 401 // resize the columns. 402 // 403 // If this function is not called after the DataGrid is attached to its 404 // parent element, then the DataGrid's columns will not be resizable. 405 updateWidths: function() 406 { 407 var headerTableColumns = this._headerTableColumnGroup.children; 408 409 var tableWidth = this._dataTable.offsetWidth; 410 var numColumns = headerTableColumns.length; 411 412 // Do not attempt to use offsetes if we're not attached to the document tree yet. 413 if (!this._columnWidthsInitialized && this.element.offsetWidth) { 414 // Give all the columns initial widths now so that during a resize, 415 // when the two columns that get resized get a percent value for 416 // their widths, all the other columns already have percent values 417 // for their widths. 418 for (var i = 0; i < numColumns; i++) { 419 var columnWidth = this.headerTableBody.rows[0].cells[i].offsetWidth; 420 var percentWidth = ((columnWidth / tableWidth) * 100) + "%"; 421 this._headerTableColumnGroup.children[i].style.width = percentWidth; 422 this._dataTableColumnGroup.children[i].style.width = percentWidth; 423 } 424 this._columnWidthsInitialized = true; 425 } 426 this._positionResizers(); 427 this.dispatchEventToListeners("width changed"); 428 }, 429 430 columnWidthsMap: function() 431 { 432 var result = {}; 433 for (var i = 0; i < this._columnsArray.length; ++i) { 434 var width = this._headerTableColumnGroup.children[i].style.width; 435 result[this._columnsArray[i].columnIdentifier] = parseFloat(width); 436 } 437 return result; 438 }, 439 440 applyColumnWidthsMap: function(columnWidthsMap) 441 { 442 for (var columnIdentifier in this.columns) { 443 var column = this.columns[columnIdentifier]; 444 var width = (columnWidthsMap[columnIdentifier] || 0) + "%"; 445 this._headerTableColumnGroup.children[column.ordinal].style.width = width; 446 this._dataTableColumnGroup.children[column.ordinal].style.width = width; 447 } 448 449 // Normalize widths 450 delete this._columnWidthsInitialized; 451 this.updateWidths(); 452 }, 453 454 isColumnVisible: function(columnIdentifier) 455 { 456 var column = this.columns[columnIdentifier]; 457 var columnElement = column.element; 458 return !columnElement.hidden; 459 }, 460 461 showColumn: function(columnIdentifier) 462 { 463 var column = this.columns[columnIdentifier]; 464 var columnElement = column.element; 465 if (!columnElement.hidden) 466 return; 467 468 columnElement.hidden = false; 469 columnElement.removeStyleClass("hidden"); 470 471 var columnBodyElement = column.bodyElement; 472 columnBodyElement.hidden = false; 473 columnBodyElement.removeStyleClass("hidden"); 474 }, 475 476 hideColumn: function(columnIdentifier) 477 { 478 var column = this.columns[columnIdentifier]; 479 var columnElement = column.element; 480 if (columnElement.hidden) 481 return; 482 483 var oldWidth = parseFloat(columnElement.style.width); 484 485 columnElement.hidden = true; 486 columnElement.addStyleClass("hidden"); 487 columnElement.style.width = 0; 488 489 var columnBodyElement = column.bodyElement; 490 columnBodyElement.hidden = true; 491 columnBodyElement.addStyleClass("hidden"); 492 columnBodyElement.style.width = 0; 493 494 this._columnWidthsInitialized = false; 495 }, 496 497 get scrollContainer() 498 { 499 return this._scrollContainer; 500 }, 501 502 isScrolledToLastRow: function() 503 { 504 return this._scrollContainer.isScrolledToBottom(); 505 }, 506 507 scrollToLastRow: function() 508 { 509 this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight - this._scrollContainer.offsetHeight; 510 }, 511 512 _positionResizers: function() 513 { 514 var headerTableColumns = this._headerTableColumnGroup.children; 515 var numColumns = headerTableColumns.length; 516 var left = 0; 517 var previousResizer = null; 518 519 // Make n - 1 resizers for n columns. 520 for (var i = 0; i < numColumns - 1; i++) { 521 var resizer = this.resizers[i]; 522 523 if (!resizer) { 524 // This is the first call to updateWidth, so the resizers need 525 // to be created. 526 resizer = document.createElement("div"); 527 resizer.addStyleClass("data-grid-resizer"); 528 // This resizer is associated with the column to its right. 529 resizer.addEventListener("mousedown", this._startResizerDragging.bind(this), false); 530 this.element.appendChild(resizer); 531 this.resizers[i] = resizer; 532 } 533 534 // Get the width of the cell in the first (and only) row of the 535 // header table in order to determine the width of the column, since 536 // it is not possible to query a column for its width. 537 left += this.headerTableBody.rows[0].cells[i].offsetWidth; 538 539 var columnIsVisible = !this._headerTableColumnGroup.children[i].hidden; 540 if (columnIsVisible) { 541 resizer.style.removeProperty("display"); 542 resizer.style.left = left + "px"; 543 resizer.leftNeighboringColumnID = i; 544 if (previousResizer) 545 previousResizer.rightNeighboringColumnID = i; 546 previousResizer = resizer; 547 } else { 548 resizer.style.setProperty("display", "none"); 549 resizer.leftNeighboringColumnID = 0; 550 resizer.rightNeighboringColumnID = 0; 551 } 552 } 553 if (previousResizer) 554 previousResizer.rightNeighboringColumnID = numColumns - 1; 555 }, 556 557 addCreationNode: function(hasChildren) 558 { 559 if (this.creationNode) 560 this.creationNode.makeNormal(); 561 562 var emptyData = {}; 563 for (var column in this.columns) 564 emptyData[column] = ''; 565 this.creationNode = new WebInspector.CreationDataGridNode(emptyData, hasChildren); 566 this.appendChild(this.creationNode); 567 }, 568 569 appendChild: function(child) 570 { 571 this.insertChild(child, this.children.length); 572 }, 573 574 insertChild: function(child, index) 575 { 576 if (!child) 577 throw("insertChild: Node can't be undefined or null."); 578 if (child.parent === this) 579 throw("insertChild: Node is already a child of this node."); 580 581 if (child.parent) 582 child.parent.removeChild(child); 583 584 this.children.splice(index, 0, child); 585 this.hasChildren = true; 586 587 child.parent = this; 588 child.dataGrid = this.dataGrid; 589 child._recalculateSiblings(index); 590 591 delete child._depth; 592 delete child._revealed; 593 delete child._attached; 594 child._shouldRefreshChildren = true; 595 596 var current = child.children[0]; 597 while (current) { 598 current.dataGrid = this.dataGrid; 599 delete current._depth; 600 delete current._revealed; 601 delete current._attached; 602 current._shouldRefreshChildren = true; 603 current = current.traverseNextNode(false, child, true); 604 } 605 606 if (this.expanded) 607 child._attach(); 608 }, 609 610 removeChild: function(child) 611 { 612 if (!child) 613 throw("removeChild: Node can't be undefined or null."); 614 if (child.parent !== this) 615 throw("removeChild: Node is not a child of this node."); 616 617 child.deselect(); 618 child._detach(); 619 620 this.children.remove(child, true); 621 622 if (child.previousSibling) 623 child.previousSibling.nextSibling = child.nextSibling; 624 if (child.nextSibling) 625 child.nextSibling.previousSibling = child.previousSibling; 626 627 child.dataGrid = null; 628 child.parent = null; 629 child.nextSibling = null; 630 child.previousSibling = null; 631 632 if (this.children.length <= 0) 633 this.hasChildren = false; 634 }, 635 636 removeChildren: function() 637 { 638 for (var i = 0; i < this.children.length; ++i) { 639 var child = this.children[i]; 640 child.deselect(); 641 child._detach(); 642 643 child.dataGrid = null; 644 child.parent = null; 645 child.nextSibling = null; 646 child.previousSibling = null; 647 } 648 649 this.children = []; 650 this.hasChildren = false; 651 }, 652 653 removeChildrenRecursive: function() 654 { 655 var childrenToRemove = this.children; 656 657 var child = this.children[0]; 658 while (child) { 659 if (child.children.length) 660 childrenToRemove = childrenToRemove.concat(child.children); 661 child = child.traverseNextNode(false, this, true); 662 } 663 664 for (var i = 0; i < childrenToRemove.length; ++i) { 665 var child = childrenToRemove[i]; 666 child.deselect(); 667 child._detach(); 668 669 child.children = []; 670 child.dataGrid = null; 671 child.parent = null; 672 child.nextSibling = null; 673 child.previousSibling = null; 674 } 675 676 this.children = []; 677 }, 678 679 sortNodes: function(comparator, reverseMode) 680 { 681 function comparatorWrapper(a, b) 682 { 683 if (a._dataGridNode._data.summaryRow) 684 return 1; 685 if (b._dataGridNode._data.summaryRow) 686 return -1; 687 688 var aDataGirdNode = a._dataGridNode; 689 var bDataGirdNode = b._dataGridNode; 690 return reverseMode ? comparator(bDataGirdNode, aDataGirdNode) : comparator(aDataGirdNode, bDataGirdNode); 691 } 692 693 var tbody = this.dataTableBody; 694 var tbodyParent = tbody.parentElement; 695 tbodyParent.removeChild(tbody); 696 697 var childNodes = tbody.childNodes; 698 var fillerRow = childNodes[childNodes.length - 1]; 699 700 var sortedRows = Array.prototype.slice.call(childNodes, 0, childNodes.length - 1); 701 sortedRows.sort(comparatorWrapper); 702 var sortedRowsLength = sortedRows.length; 703 704 tbody.removeChildren(); 705 var previousSiblingNode = null; 706 for (var i = 0; i < sortedRowsLength; ++i) { 707 var row = sortedRows[i]; 708 var node = row._dataGridNode; 709 node.previousSibling = previousSiblingNode; 710 if (previousSiblingNode) 711 previousSiblingNode.nextSibling = node; 712 tbody.appendChild(row); 713 previousSiblingNode = node; 714 } 715 if (previousSiblingNode) 716 previousSiblingNode.nextSibling = null; 717 718 tbody.appendChild(fillerRow); 719 tbodyParent.appendChild(tbody); 720 }, 721 722 _keyDown: function(event) 723 { 724 if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing) 725 return; 726 727 var handled = false; 728 var nextSelectedNode; 729 if (event.keyIdentifier === "Up" && !event.altKey) { 730 nextSelectedNode = this.selectedNode.traversePreviousNode(true); 731 while (nextSelectedNode && !nextSelectedNode.selectable) 732 nextSelectedNode = nextSelectedNode.traversePreviousNode(!this.expandTreeNodesWhenArrowing); 733 handled = nextSelectedNode ? true : false; 734 } else if (event.keyIdentifier === "Down" && !event.altKey) { 735 nextSelectedNode = this.selectedNode.traverseNextNode(true); 736 while (nextSelectedNode && !nextSelectedNode.selectable) 737 nextSelectedNode = nextSelectedNode.traverseNextNode(!this.expandTreeNodesWhenArrowing); 738 handled = nextSelectedNode ? true : false; 739 } else if (event.keyIdentifier === "Left") { 740 if (this.selectedNode.expanded) { 741 if (event.altKey) 742 this.selectedNode.collapseRecursively(); 743 else 744 this.selectedNode.collapse(); 745 handled = true; 746 } else if (this.selectedNode.parent && !this.selectedNode.parent.root) { 747 handled = true; 748 if (this.selectedNode.parent.selectable) { 749 nextSelectedNode = this.selectedNode.parent; 750 handled = nextSelectedNode ? true : false; 751 } else if (this.selectedNode.parent) 752 this.selectedNode.parent.collapse(); 753 } 754 } else if (event.keyIdentifier === "Right") { 755 if (!this.selectedNode.revealed) { 756 this.selectedNode.reveal(); 757 handled = true; 758 } else if (this.selectedNode.hasChildren) { 759 handled = true; 760 if (this.selectedNode.expanded) { 761 nextSelectedNode = this.selectedNode.children[0]; 762 handled = nextSelectedNode ? true : false; 763 } else { 764 if (event.altKey) 765 this.selectedNode.expandRecursively(); 766 else 767 this.selectedNode.expand(); 768 } 769 } 770 } else if (event.keyCode === 8 || event.keyCode === 46) { 771 if (this._deleteCallback) { 772 handled = true; 773 this._deleteCallback(this.selectedNode); 774 } 775 } else if (isEnterKey(event)) { 776 if (this._editCallback) { 777 handled = true; 778 // The first child of the selected element is the <td class="0-column">, 779 // and that's what we want to edit. 780 this._startEditing(this.selectedNode._element.children[0]); 781 } 782 } 783 784 if (nextSelectedNode) { 785 nextSelectedNode.reveal(); 786 nextSelectedNode.select(); 787 } 788 789 if (handled) { 790 event.preventDefault(); 791 event.stopPropagation(); 792 } 793 }, 794 795 expand: function() 796 { 797 // This is the root, do nothing. 798 }, 799 800 collapse: function() 801 { 802 // This is the root, do nothing. 803 }, 804 805 reveal: function() 806 { 807 // This is the root, do nothing. 808 }, 809 810 dataGridNodeFromNode: function(target) 811 { 812 var rowElement = target.enclosingNodeOrSelfWithNodeName("tr"); 813 return rowElement && rowElement._dataGridNode; 814 }, 815 816 dataGridNodeFromPoint: function(x, y) 817 { 818 var node = this._dataTable.ownerDocument.elementFromPoint(x, y); 819 var rowElement = node.enclosingNodeOrSelfWithNodeName("tr"); 820 return rowElement && rowElement._dataGridNode; 821 }, 822 823 _clickInHeaderCell: function(event) 824 { 825 var cell = event.target.enclosingNodeOrSelfWithNodeName("th"); 826 if (!cell || !cell.columnIdentifier || !cell.hasStyleClass("sortable")) 827 return; 828 829 var sortOrder = this.sortOrder; 830 831 if (this._sortColumnCell) 832 this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+"); 833 834 if (cell == this._sortColumnCell) { 835 if (sortOrder === "ascending") 836 sortOrder = "descending"; 837 else 838 sortOrder = "ascending"; 839 } 840 841 this._sortColumnCell = cell; 842 843 cell.addStyleClass("sort-" + sortOrder); 844 845 this.dispatchEventToListeners("sorting changed"); 846 }, 847 848 markColumnAsSortedBy: function(columnIdentifier, sortOrder) 849 { 850 if (this._sortColumnCell) 851 this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+"); 852 this._sortColumnCell = this._headerTableHeaders[columnIdentifier]; 853 this._sortColumnCell.addStyleClass("sort-" + sortOrder); 854 }, 855 856 headerTableHeader: function(columnIdentifier) 857 { 858 return this._headerTableHeaders[columnIdentifier]; 859 }, 860 861 _mouseDownInDataTable: function(event) 862 { 863 var gridNode = this.dataGridNodeFromNode(event.target); 864 if (!gridNode || !gridNode.selectable) 865 return; 866 867 if (gridNode.isEventWithinDisclosureTriangle(event)) 868 return; 869 870 if (event.metaKey) { 871 if (gridNode.selected) 872 gridNode.deselect(); 873 else 874 gridNode.select(); 875 } else 876 gridNode.select(); 877 }, 878 879 _contextMenuInDataTable: function(event) 880 { 881 var gridNode = this.dataGridNodeFromNode(event.target); 882 if (!gridNode || !gridNode.selectable) 883 return; 884 885 if (gridNode.isEventWithinDisclosureTriangle(event)) 886 return; 887 888 var contextMenu = new WebInspector.ContextMenu(); 889 890 // FIXME: Use the column names for Editing, instead of just "Edit". 891 if (this.dataGrid._editCallback) { 892 if (gridNode === this.creationNode) 893 contextMenu.appendItem(WebInspector.UIString("Add New"), this._startEditing.bind(this, event.target)); 894 else 895 contextMenu.appendItem(WebInspector.UIString("Edit"), this._startEditing.bind(this, event.target)); 896 } 897 if (this.dataGrid._deleteCallback && gridNode !== this.creationNode) 898 contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode)); 899 900 contextMenu.show(event); 901 }, 902 903 _clickInDataTable: function(event) 904 { 905 var gridNode = this.dataGridNodeFromNode(event.target); 906 if (!gridNode || !gridNode.hasChildren) 907 return; 908 909 if (!gridNode.isEventWithinDisclosureTriangle(event)) 910 return; 911 912 if (gridNode.expanded) { 913 if (event.altKey) 914 gridNode.collapseRecursively(); 915 else 916 gridNode.collapse(); 917 } else { 918 if (event.altKey) 919 gridNode.expandRecursively(); 920 else 921 gridNode.expand(); 922 } 923 }, 924 925 _startResizerDragging: function(event) 926 { 927 this.currentResizer = event.target; 928 if (!this.currentResizer.rightNeighboringColumnID) 929 return; 930 WebInspector.elementDragStart(this.lastResizer, this._resizerDragging.bind(this), 931 this._endResizerDragging.bind(this), event, "col-resize"); 932 }, 933 934 _resizerDragging: function(event) 935 { 936 var resizer = this.currentResizer; 937 if (!resizer) 938 return; 939 940 // Constrain the dragpoint to be within the containing div of the 941 // datagrid. 942 var dragPoint = event.clientX - this.element.totalOffsetLeft; 943 // Constrain the dragpoint to be within the space made up by the 944 // column directly to the left and the column directly to the right. 945 var leftEdgeOfPreviousColumn = 0; 946 var firstRowCells = this.headerTableBody.rows[0].cells; 947 for (var i = 0; i < resizer.leftNeighboringColumnID; i++) 948 leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth; 949 950 var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[resizer.leftNeighboringColumnID].offsetWidth + firstRowCells[resizer.rightNeighboringColumnID].offsetWidth; 951 952 // Give each column some padding so that they don't disappear. 953 var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding; 954 var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding; 955 956 dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum); 957 958 resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px"; 959 960 var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / this._dataTable.offsetWidth) * 100) + "%"; 961 this._headerTableColumnGroup.children[resizer.leftNeighboringColumnID].style.width = percentLeftColumn; 962 this._dataTableColumnGroup.children[resizer.leftNeighboringColumnID].style.width = percentLeftColumn; 963 964 var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / this._dataTable.offsetWidth) * 100) + "%"; 965 this._headerTableColumnGroup.children[resizer.rightNeighboringColumnID].style.width = percentRightColumn; 966 this._dataTableColumnGroup.children[resizer.rightNeighboringColumnID].style.width = percentRightColumn; 967 968 this._positionResizers(); 969 event.preventDefault(); 970 this.dispatchEventToListeners("width changed"); 971 }, 972 973 _endResizerDragging: function(event) 974 { 975 WebInspector.elementDragEnd(event); 976 this.currentResizer = null; 977 this.dispatchEventToListeners("width changed"); 978 }, 979 980 ColumnResizePadding: 10, 981 982 CenterResizerOverBorderAdjustment: 3, 983} 984 985WebInspector.DataGrid.prototype.__proto__ = WebInspector.Object.prototype; 986 987WebInspector.DataGridNode = function(data, hasChildren) 988{ 989 this._expanded = false; 990 this._selected = false; 991 this._shouldRefreshChildren = true; 992 this._data = data || {}; 993 this.hasChildren = hasChildren || false; 994 this.children = []; 995 this.dataGrid = null; 996 this.parent = null; 997 this.previousSibling = null; 998 this.nextSibling = null; 999 this.disclosureToggleWidth = 10; 1000} 1001 1002WebInspector.DataGridNode.prototype = { 1003 selectable: true, 1004 1005 get element() 1006 { 1007 if (this._element) 1008 return this._element; 1009 1010 if (!this.dataGrid) 1011 return null; 1012 1013 this._element = document.createElement("tr"); 1014 this._element._dataGridNode = this; 1015 1016 if (this.hasChildren) 1017 this._element.addStyleClass("parent"); 1018 if (this.expanded) 1019 this._element.addStyleClass("expanded"); 1020 if (this.selected) 1021 this._element.addStyleClass("selected"); 1022 if (this.revealed) 1023 this._element.addStyleClass("revealed"); 1024 1025 this.createCells(); 1026 return this._element; 1027 }, 1028 1029 createCells: function() 1030 { 1031 for (var columnIdentifier in this.dataGrid.columns) { 1032 var cell = this.createCell(columnIdentifier); 1033 this._element.appendChild(cell); 1034 } 1035 }, 1036 1037 get data() 1038 { 1039 return this._data; 1040 }, 1041 1042 set data(x) 1043 { 1044 this._data = x || {}; 1045 this.refresh(); 1046 }, 1047 1048 get revealed() 1049 { 1050 if ("_revealed" in this) 1051 return this._revealed; 1052 1053 var currentAncestor = this.parent; 1054 while (currentAncestor && !currentAncestor.root) { 1055 if (!currentAncestor.expanded) { 1056 this._revealed = false; 1057 return false; 1058 } 1059 1060 currentAncestor = currentAncestor.parent; 1061 } 1062 1063 this._revealed = true; 1064 return true; 1065 }, 1066 1067 set hasChildren(x) 1068 { 1069 if (this._hasChildren === x) 1070 return; 1071 1072 this._hasChildren = x; 1073 1074 if (!this._element) 1075 return; 1076 1077 if (this._hasChildren) 1078 { 1079 this._element.addStyleClass("parent"); 1080 if (this.expanded) 1081 this._element.addStyleClass("expanded"); 1082 } 1083 else 1084 { 1085 this._element.removeStyleClass("parent"); 1086 this._element.removeStyleClass("expanded"); 1087 } 1088 }, 1089 1090 get hasChildren() 1091 { 1092 return this._hasChildren; 1093 }, 1094 1095 set revealed(x) 1096 { 1097 if (this._revealed === x) 1098 return; 1099 1100 this._revealed = x; 1101 1102 if (this._element) { 1103 if (this._revealed) 1104 this._element.addStyleClass("revealed"); 1105 else 1106 this._element.removeStyleClass("revealed"); 1107 } 1108 1109 for (var i = 0; i < this.children.length; ++i) 1110 this.children[i].revealed = x && this.expanded; 1111 }, 1112 1113 get depth() 1114 { 1115 if ("_depth" in this) 1116 return this._depth; 1117 if (this.parent && !this.parent.root) 1118 this._depth = this.parent.depth + 1; 1119 else 1120 this._depth = 0; 1121 return this._depth; 1122 }, 1123 1124 get shouldRefreshChildren() 1125 { 1126 return this._shouldRefreshChildren; 1127 }, 1128 1129 set shouldRefreshChildren(x) 1130 { 1131 this._shouldRefreshChildren = x; 1132 if (x && this.expanded) 1133 this.expand(); 1134 }, 1135 1136 get selected() 1137 { 1138 return this._selected; 1139 }, 1140 1141 set selected(x) 1142 { 1143 if (x) 1144 this.select(); 1145 else 1146 this.deselect(); 1147 }, 1148 1149 get expanded() 1150 { 1151 return this._expanded; 1152 }, 1153 1154 set expanded(x) 1155 { 1156 if (x) 1157 this.expand(); 1158 else 1159 this.collapse(); 1160 }, 1161 1162 refresh: function() 1163 { 1164 if (!this._element || !this.dataGrid) 1165 return; 1166 1167 this._element.removeChildren(); 1168 this.createCells(); 1169 }, 1170 1171 createCell: function(columnIdentifier) 1172 { 1173 var cell = document.createElement("td"); 1174 cell.className = columnIdentifier + "-column"; 1175 1176 var alignment = this.dataGrid.aligned[columnIdentifier]; 1177 if (alignment) 1178 cell.addStyleClass(alignment); 1179 1180 var div = document.createElement("div"); 1181 div.textContent = this.data[columnIdentifier]; 1182 cell.appendChild(div); 1183 1184 if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) { 1185 cell.addStyleClass("disclosure"); 1186 if (this.depth) 1187 cell.style.setProperty("padding-left", (this.depth * this.dataGrid.indentWidth) + "px"); 1188 } 1189 1190 return cell; 1191 }, 1192 1193 // Share these functions with DataGrid. They are written to work with a DataGridNode this object. 1194 appendChild: WebInspector.DataGrid.prototype.appendChild, 1195 insertChild: WebInspector.DataGrid.prototype.insertChild, 1196 removeChild: WebInspector.DataGrid.prototype.removeChild, 1197 removeChildren: WebInspector.DataGrid.prototype.removeChildren, 1198 removeChildrenRecursive: WebInspector.DataGrid.prototype.removeChildrenRecursive, 1199 1200 _recalculateSiblings: function(myIndex) 1201 { 1202 if (!this.parent) 1203 return; 1204 1205 var previousChild = (myIndex > 0 ? this.parent.children[myIndex - 1] : null); 1206 1207 if (previousChild) { 1208 previousChild.nextSibling = this; 1209 this.previousSibling = previousChild; 1210 } else 1211 this.previousSibling = null; 1212 1213 var nextChild = this.parent.children[myIndex + 1]; 1214 1215 if (nextChild) { 1216 nextChild.previousSibling = this; 1217 this.nextSibling = nextChild; 1218 } else 1219 this.nextSibling = null; 1220 }, 1221 1222 collapse: function() 1223 { 1224 if (this._element) 1225 this._element.removeStyleClass("expanded"); 1226 1227 this._expanded = false; 1228 1229 for (var i = 0; i < this.children.length; ++i) 1230 this.children[i].revealed = false; 1231 1232 this.dispatchEventToListeners("collapsed"); 1233 }, 1234 1235 collapseRecursively: function() 1236 { 1237 var item = this; 1238 while (item) { 1239 if (item.expanded) 1240 item.collapse(); 1241 item = item.traverseNextNode(false, this, true); 1242 } 1243 }, 1244 1245 expand: function() 1246 { 1247 if (!this.hasChildren || this.expanded) 1248 return; 1249 1250 if (this.revealed && !this._shouldRefreshChildren) 1251 for (var i = 0; i < this.children.length; ++i) 1252 this.children[i].revealed = true; 1253 1254 if (this._shouldRefreshChildren) { 1255 for (var i = 0; i < this.children.length; ++i) 1256 this.children[i]._detach(); 1257 1258 this.dispatchEventToListeners("populate"); 1259 1260 if (this._attached) { 1261 for (var i = 0; i < this.children.length; ++i) { 1262 var child = this.children[i]; 1263 if (this.revealed) 1264 child.revealed = true; 1265 child._attach(); 1266 } 1267 } 1268 1269 delete this._shouldRefreshChildren; 1270 } 1271 1272 if (this._element) 1273 this._element.addStyleClass("expanded"); 1274 1275 this._expanded = true; 1276 1277 this.dispatchEventToListeners("expanded"); 1278 }, 1279 1280 expandRecursively: function() 1281 { 1282 var item = this; 1283 while (item) { 1284 item.expand(); 1285 item = item.traverseNextNode(false, this); 1286 } 1287 }, 1288 1289 reveal: function() 1290 { 1291 var currentAncestor = this.parent; 1292 while (currentAncestor && !currentAncestor.root) { 1293 if (!currentAncestor.expanded) 1294 currentAncestor.expand(); 1295 currentAncestor = currentAncestor.parent; 1296 } 1297 1298 this.element.scrollIntoViewIfNeeded(false); 1299 1300 this.dispatchEventToListeners("revealed"); 1301 }, 1302 1303 select: function(supressSelectedEvent) 1304 { 1305 if (!this.dataGrid || !this.selectable || this.selected) 1306 return; 1307 1308 if (this.dataGrid.selectedNode) 1309 this.dataGrid.selectedNode.deselect(); 1310 1311 this._selected = true; 1312 this.dataGrid.selectedNode = this; 1313 1314 if (this._element) 1315 this._element.addStyleClass("selected"); 1316 1317 if (!supressSelectedEvent) 1318 this.dispatchEventToListeners("selected"); 1319 }, 1320 1321 deselect: function(supressDeselectedEvent) 1322 { 1323 if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected) 1324 return; 1325 1326 this._selected = false; 1327 this.dataGrid.selectedNode = null; 1328 1329 if (this._element) 1330 this._element.removeStyleClass("selected"); 1331 1332 if (!supressDeselectedEvent) 1333 this.dispatchEventToListeners("deselected"); 1334 }, 1335 1336 traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info) 1337 { 1338 if (!dontPopulate && this.hasChildren) 1339 this.dispatchEventToListeners("populate"); 1340 1341 if (info) 1342 info.depthChange = 0; 1343 1344 var node = (!skipHidden || this.revealed) ? this.children[0] : null; 1345 if (node && (!skipHidden || this.expanded)) { 1346 if (info) 1347 info.depthChange = 1; 1348 return node; 1349 } 1350 1351 if (this === stayWithin) 1352 return null; 1353 1354 node = (!skipHidden || this.revealed) ? this.nextSibling : null; 1355 if (node) 1356 return node; 1357 1358 node = this; 1359 while (node && !node.root && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) { 1360 if (info) 1361 info.depthChange -= 1; 1362 node = node.parent; 1363 } 1364 1365 if (!node) 1366 return null; 1367 1368 return (!skipHidden || node.revealed) ? node.nextSibling : null; 1369 }, 1370 1371 traversePreviousNode: function(skipHidden, dontPopulate) 1372 { 1373 var node = (!skipHidden || this.revealed) ? this.previousSibling : null; 1374 if (!dontPopulate && node && node.hasChildren) 1375 node.dispatchEventToListeners("populate"); 1376 1377 while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null)) { 1378 if (!dontPopulate && node.hasChildren) 1379 node.dispatchEventToListeners("populate"); 1380 node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null); 1381 } 1382 1383 if (node) 1384 return node; 1385 1386 if (!this.parent || this.parent.root) 1387 return null; 1388 1389 return this.parent; 1390 }, 1391 1392 isEventWithinDisclosureTriangle: function(event) 1393 { 1394 if (!this.hasChildren) 1395 return false; 1396 var cell = event.target.enclosingNodeOrSelfWithNodeName("td"); 1397 if (!cell.hasStyleClass("disclosure")) 1398 return false; 1399 var computedLeftPadding = window.getComputedStyle(cell).getPropertyCSSValue("padding-left").getFloatValue(CSSPrimitiveValue.CSS_PX); 1400 var left = cell.totalOffsetLeft + computedLeftPadding; 1401 return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth; 1402 }, 1403 1404 _attach: function() 1405 { 1406 if (!this.dataGrid || this._attached) 1407 return; 1408 1409 this._attached = true; 1410 1411 var nextNode = null; 1412 var previousNode = this.traversePreviousNode(true, true); 1413 if (previousNode && previousNode.element.parentNode && previousNode.element.nextSibling) 1414 var nextNode = previousNode.element.nextSibling; 1415 if (!nextNode) 1416 nextNode = this.dataGrid.dataTableBody.lastChild; 1417 this.dataGrid.dataTableBody.insertBefore(this.element, nextNode); 1418 1419 if (this.expanded) 1420 for (var i = 0; i < this.children.length; ++i) 1421 this.children[i]._attach(); 1422 }, 1423 1424 _detach: function() 1425 { 1426 if (!this._attached) 1427 return; 1428 1429 this._attached = false; 1430 1431 if (this._element && this._element.parentNode) 1432 this._element.parentNode.removeChild(this._element); 1433 1434 for (var i = 0; i < this.children.length; ++i) 1435 this.children[i]._detach(); 1436 }, 1437 1438 savePosition: function() 1439 { 1440 if (this._savedPosition) 1441 return; 1442 1443 if (!this.parent) 1444 throw("savePosition: Node must have a parent."); 1445 this._savedPosition = { 1446 parent: this.parent, 1447 index: this.parent.children.indexOf(this) 1448 }; 1449 }, 1450 1451 restorePosition: function() 1452 { 1453 if (!this._savedPosition) 1454 return; 1455 1456 if (this.parent !== this._savedPosition.parent) 1457 this._savedPosition.parent.insertChild(this, this._savedPosition.index); 1458 1459 delete this._savedPosition; 1460 } 1461} 1462 1463WebInspector.DataGridNode.prototype.__proto__ = WebInspector.Object.prototype; 1464 1465WebInspector.CreationDataGridNode = function(data, hasChildren) 1466{ 1467 WebInspector.DataGridNode.call(this, data, hasChildren); 1468 this.isCreationNode = true; 1469} 1470 1471WebInspector.CreationDataGridNode.prototype = { 1472 makeNormal: function() 1473 { 1474 delete this.isCreationNode; 1475 delete this.makeNormal; 1476 } 1477} 1478 1479WebInspector.CreationDataGridNode.prototype.__proto__ = WebInspector.DataGridNode.prototype; 1480