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