Home | History | Annotate | Download | only in front-end
      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