text: /*
   * Copyright (C) 2008 Apple Inc. All Rights Reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions
   * are met:
   * 1. Redistributions of source code must retain the above copyright
   *        notice, this list of conditions and the following disclaimer.
   * 2. Redistributions in binary form must reproduce the above copyright
   *        notice, this list of conditions and the following disclaimer in the
   *        documentation and/or other materials provided with the distribution.
   *
   * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
   * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
   * PURPOSE ARE DISCLAIMED.         IN NO EVENT SHALL APPLE INC. OR
   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
   * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
   * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
   * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */
  
  WebInspector.DataGrid = function(columns, editCallback, deleteCallback)
  {
      this.element = document.createElement("div");
      this.element.className = "data-grid";
      this.element.tabIndex = 0;
      this.element.addEventListener("keydown", this._keyDown.bind(this), false);
  
      this._headerTable = document.createElement("table");
      this._headerTable.className = "header";
      this._headerTableHeaders = {};
  
      this._dataTable = document.createElement("table");
      this._dataTable.className = "data";
  
      this._dataTable.addEventListener("mousedown", this._mouseDownInDataTable.bind(this), true);
      this._dataTable.addEventListener("click", this._clickInDataTable.bind(this), true);
      
      this._dataTable.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true);
      
      // FIXME: Add a createCallback which is different from editCallback and has different
      // behavior when creating a new node.
      if (editCallback) {
          this._dataTable.addEventListener("dblclick", this._ondblclick.bind(this), false);
          this._editCallback = editCallback;
      }
      if (deleteCallback)
          this._deleteCallback = deleteCallback;
      
      this.aligned = {};
  
      this._scrollContainer = document.createElement("div");
      this._scrollContainer.className = "data-container";
      this._scrollContainer.appendChild(this._dataTable);
  
      this.element.appendChild(this._headerTable);
      this.element.appendChild(this._scrollContainer);
  
      var headerRow = document.createElement("tr");
      var columnGroup = document.createElement("colgroup");
      this._columnCount = 0;
  
      for (var columnIdentifier in columns) {
          var column = columns[columnIdentifier];
          if (column.disclosure)
              this.disclosureColumnIdentifier = columnIdentifier;
  
          var col = document.createElement("col");
          if (column.width)
              col.style.width = column.width;
          column.element = col;
          columnGroup.appendChild(col);
  
          var cell = document.createElement("th");
          cell.className = columnIdentifier + "-column";
          cell.columnIdentifier = columnIdentifier;
          this._headerTableHeaders[columnIdentifier] = cell;
  
          var div = document.createElement("div");
          if (column.titleDOMFragment)
              div.appendChild(column.titleDOMFragment);
          else
              div.textContent = column.title;
          cell.appendChild(div);
  
          if (column.sort) {
              cell.addStyleClass("sort-" + column.sort);
              this._sortColumnCell = cell;
          }
  
          if (column.sortable) {
              cell.addEventListener("click", this._clickInHeaderCell.bind(this), false);
              cell.addStyleClass("sortable");
          }
  
          if (column.aligned)
              this.aligned[columnIdentifier] = column.aligned;
  
          headerRow.appendChild(cell);
  
          ++this._columnCount;
      }
  
      columnGroup.span = this._columnCount;
  
      var cell = document.createElement("th");
      cell.className = "corner";
      headerRow.appendChild(cell);
  
      this._headerTableColumnGroup = columnGroup;
      this._headerTable.appendChild(this._headerTableColumnGroup);
      this.headerTableBody.appendChild(headerRow);
  
      var fillerRow = document.createElement("tr");
      fillerRow.className = "filler";
  
      for (var columnIdentifier in columns) {
          var column = columns[columnIdentifier];
          var cell = document.createElement("td");
          cell.className = columnIdentifier + "-column";
          fillerRow.appendChild(cell);
      }
  
      this._dataTableColumnGroup = columnGroup.cloneNode(true);
      this._dataTable.appendChild(this._dataTableColumnGroup);
      this.dataTableBody.appendChild(fillerRow);
  
      this.columns = columns || {};
      this._columnsArray = [];
      for (var columnIdentifier in columns) {
          columns[columnIdentifier].ordinal = this._columnsArray.length;
          columns[columnIdentifier].identifier = columnIdentifier;
          this._columnsArray.push(columns[columnIdentifier]);
      }
  
      for (var i = 0; i < this._columnsArray.length; ++i)
          this._columnsArray[i].bodyElement = this._dataTableColumnGroup.children[i];
  
      this.children = [];
      this.selectedNode = null;
      this.expandNodesWhenArrowing = false;
      this.root = true;
      this.hasChildren = false;
      this.expanded = true;
      this.revealed = true;
      this.selected = false;
      this.dataGrid = this;
      this.indentWidth = 15;
      this.resizers = [];
      this._columnWidthsInitialized = false;
  }
  
  WebInspector.DataGrid.prototype = {
      _ondblclick: function(event)
      {
          if (this._editing || this._editingNode)
              return;
  
          this._startEditing(event.target);
      },
  
      _startEditingColumnOfDataGridNode: function(node, column)
      {
          this._editing = true;
          this._editingNode = node;
          this._editingNode.select();
  
          var element = this._editingNode._element.children[column];
          WebInspector.startEditing(element, {
              context: element.textContent,
              commitHandler: this._editingCommitted.bind(this),
              cancelHandler: this._editingCancelled.bind(this)
          });
          window.getSelection().setBaseAndExtent(element, 0, element, 1);
      },
  
      _startEditing: function(target)
      {
          var element = target.enclosingNodeOrSelfWithNodeName("td");
          if (!element)
              return;
  
          this._editingNode = this.dataGridNodeFromNode(target);
          if (!this._editingNode) {
              if (!this.creationNode)
                  return;
              this._editingNode = this.creationNode;
          }
  
          // Force editing the 1st column when editing the creation node
          if (this._editingNode.isCreationNode)
              return this._startEditingColumnOfDataGridNode(this._editingNode, 0);
  
          this._editing = true;
          WebInspector.startEditing(element, {
              context: element.textContent,
              commitHandler: this._editingCommitted.bind(this),
              cancelHandler: this._editingCancelled.bind(this)
          });
          window.getSelection().setBaseAndExtent(element, 0, element, 1);
      },
  
      _editingCommitted: function(element, newText, oldText, context, moveDirection)
      {
          // FIXME: We need more column identifiers here throughout this function.
          // Not needed yet since only editable DataGrid is DOM Storage, which is Key - Value.
          
          // FIXME: Better way to do this than regular expressions?
          var columnIdentifier = parseInt(element.className.match(/\b(\d+)-column\b/)[1]);
  
          var textBeforeEditing = this._editingNode.data[columnIdentifier];
          var currentEditingNode = this._editingNode;
  
          function moveToNextIfNeeded(wasChange) {
              if (!moveDirection)
                  return;
  
              if (moveDirection === "forward") {
                  if (currentEditingNode.isCreationNode && columnIdentifier === 0 && !wasChange)
                      return;
  
                  if (columnIdentifier === 0)
                      return this._startEditingColumnOfDataGridNode(currentEditingNode, 1);
  
                  var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true);
                  if (nextDataGridNode)
                      return this._startEditingColumnOfDataGridNode(nextDataGridNode, 0);
                  if (currentEditingNode.isCreationNode && wasChange) {
                      addCreationNode(false);
                      return this._startEditingColumnOfDataGridNode(this.creationNode, 0);
                  }
                  return;
              }
  
              if (moveDirection === "backward") {
                  if (columnIdentifier === 1)
                      return this._startEditingColumnOfDataGridNode(currentEditingNode, 0);
                      var nextDataGridNode = currentEditingNode.traversePreviousNode(true, null, true);
  
                  if (nextDataGridNode)
                      return this._startEditingColumnOfDataGridNode(nextDataGridNode, 1);
                  return;
              }
          }
  
          if (textBeforeEditing == newText) {
              this._editingCancelled(element);
              moveToNextIfNeeded.call(this, false);
              return;
          }
  
          // Update the text in the datagrid that we typed
          this._editingNode.data[columnIdentifier] = newText;
          
          // Make the callback - expects an editing node (table row), the column number that is being edited,
          // the text that used to be there, and the new text.
          this._editCallback(this._editingNode, columnIdentifier, textBeforeEditing, newText);
  
          if (this._editingNode.isCreationNode)
              this.addCreationNode(false);
  
          this._editingCancelled(element);
          moveToNextIfNeeded.call(this, true);
      },
  
      _editingCancelled: function(element, context)
      {
          delete this._editing;
          this._editingNode = null;
      },
      
      get sortColumnIdentifier()
      {
          if (!this._sortColumnCell)
              return null;
          return this._sortColumnCell.columnIdentifier;
      },
  
      get sortOrder()
      {
          if (!this._sortColumnCell || this._sortColumnCell.hasStyleClass("sort-ascending"))
              return "ascending";
          if (this._sortColumnCell.hasStyleClass("sort-descending"))
              return "descending";
          return null;
      },
  
      get headerTableBody()
      {
          if ("_headerTableBody" in this)
              return this._headerTableBody;
  
          this._headerTableBody = this._headerTable.getElementsByTagName("tbody")[0];
          if (!this._headerTableBody) {
              this._headerTableBody = this.element.ownerDocument.createElement("tbody");
              this._headerTable.insertBefore(this._headerTableBody, this._headerTable.tFoot);
          }
  
          return this._headerTableBody;
      },
  
      get dataTableBody()
      {
          if ("_dataTableBody" in this)
              return this._dataTableBody;
  
          this._dataTableBody = this._dataTable.getElementsByTagName("tbody")[0];
          if (!this._dataTableBody) {
              this._dataTableBody = this.element.ownerDocument.createElement("tbody");
              this._dataTable.insertBefore(this._dataTableBody, this._dataTable.tFoot);
          }
  
          return this._dataTableBody;
      },
  
      autoSizeColumns: function(minPercent, maxPercent, maxDescentLevel)
      {
          if (minPercent)
              minPercent = Math.min(minPercent, Math.floor(100 / this._columnCount));
          var widths = {};
          var columns = this.columns;
          for (var columnIdentifier in columns)
              widths[columnIdentifier] = (columns[columnIdentifier].title || "").length;
  
          var children = maxDescentLevel ? this._enumerateChildren(this, [], maxDescentLevel + 1) : this.children;
          for (var i = 0; i < children.length; ++i) {
              var node = children[i];
              for (var columnIdentifier in columns) {
                  var text = node.data[columnIdentifier] || "";
                  if (text.length > widths[columnIdentifier])
                      widths[columnIdentifier] = text.length;
              }
          }
  
          var totalColumnWidths = 0;
          for (var columnIdentifier in columns)
              totalColumnWidths += widths[columnIdentifier];
  
          var recoupPercent = 0;
          for (var columnIdentifier in columns) {
              var width = Math.round(100 * widths[columnIdentifier] / totalColumnWidths);
              if (minPercent && width < minPercent) {
                  recoupPercent += (minPercent - width);
                  width = minPercent;
              } else if (maxPercent && width > maxPercent) {
                  recoupPercent -= (width - maxPercent);
                  width = maxPercent;
              }
              widths[columnIdentifier] = width;
          }
  
          while (minPercent && recoupPercent > 0) {
              for (var columnIdentifier in columns) {
                  if (widths[columnIdentifier] > minPercent) {
                      --widths[columnIdentifier];
                      --recoupPercent;
                      if (!recoupPercent)
                          break;
                  }
              }
          }
  
          while (maxPercent && recoupPercent < 0) {
              for (var columnIdentifier in columns) {
                  if (widths[columnIdentifier] < maxPercent) {
                      ++widths[columnIdentifier];
                      ++recoupPercent;
                      if (!recoupPercent)
                          break;
                  }
              }
          }
  
          for (var columnIdentifier in columns)
              columns[columnIdentifier].element.style.width = widths[columnIdentifier] + "%";
          this._columnWidthsInitialized = false;
          this.updateWidths();
      },
  
      _enumerateChildren: function(rootNode, result, maxLevel)
      {
          if (!rootNode.root)
              result.push(rootNode);
          if (!maxLevel)
              return;
          for (var i = 0; i < rootNode.children.length; ++i)
              this._enumerateChildren(rootNode.children[i], result, maxLevel - 1);
          return result;
      },
  
      // Updates the widths of the table, including the positions of the column
      // resizers.
      //
      // IMPORTANT: This function MUST be called once after the element of the
      // DataGrid is attached to its parent element and every subsequent time the
      // width of the parent element is changed in order to make it possible to
      // resize the columns.
      //
      // If this function is not called after the DataGrid is attached to its
      // parent element, then the DataGrid's columns will not be resizable.
      updateWidths: function()
      {
          var headerTableColumns = this._headerTableColumnGroup.children;
          
          var tableWidth = this._dataTable.offsetWidth;
          var numColumns = headerTableColumns.length;
          
          // Do not attempt to use offsetes if we're not attached to the document tree yet.
          if (!this._columnWidthsInitialized && this.element.offsetWidth) {
              // Give all the columns initial widths now so that during a resize,
              // when the two columns that get resized get a percent value for
              // their widths, all the other columns already have percent values
              // for their widths.
              for (var i = 0; i < numColumns; i++) {
                  var columnWidth = this.headerTableBody.rows[0].cells[i].offsetWidth;
                  var percentWidth = ((columnWidth / tableWidth) * 100) + "%";
                  this._headerTableColumnGroup.children[i].style.width = percentWidth;
                  this._dataTableColumnGroup.children[i].style.width = percentWidth;
              }
              this._columnWidthsInitialized = true;
          }
          this._positionResizers();
          this.dispatchEventToListeners("width changed");
      },
  
      columnWidthsMap: function()
      {
          var result = {};
          for (var i = 0; i < this._columnsArray.length; ++i) {
              var width = this._headerTableColumnGroup.children[i].style.width;
              result[this._columnsArray[i].columnIdentifier] = parseFloat(width);
          }
          return result;
      },
  
      applyColumnWidthsMap: function(columnWidthsMap)
      {
          for (var columnIdentifier in this.columns) {
              var column = this.columns[columnIdentifier];
              var width = (columnWidthsMap[columnIdentifier] || 0) + "%";
              this._headerTableColumnGroup.children[column.ordinal].style.width = width;
              this._dataTableColumnGroup.children[column.ordinal].style.width = width;
          }
  
          // Normalize widths
          delete this._columnWidthsInitialized;
          this.updateWidths();
      },
  
      isColumnVisible: function(columnIdentifier)
      {
          var column = this.columns[columnIdentifier];
          var columnElement = column.element;
          return !columnElement.hidden;
      },
  
      showColumn: function(columnIdentifier)
      {
          var column = this.columns[columnIdentifier];
          var columnElement = column.element;
          if (!columnElement.hidden)
              return;
  
          columnElement.hidden = false;
          columnElement.removeStyleClass("hidden");
  
          var columnBodyElement = column.bodyElement;
          columnBodyElement.hidden = false;
          columnBodyElement.removeStyleClass("hidden");
      },
  
      hideColumn: function(columnIdentifier)
      {
          var column = this.columns[columnIdentifier];
          var columnElement = column.element;
          if (columnElement.hidden)
              return;
  
          var oldWidth = parseFloat(columnElement.style.width);
  
          columnElement.hidden = true;
          columnElement.addStyleClass("hidden");
          columnElement.style.width = 0;
  
          var columnBodyElement = column.bodyElement;
          columnBodyElement.hidden = true;
          columnBodyElement.addStyleClass("hidden");
          columnBodyElement.style.width = 0;
  
          this._columnWidthsInitialized = false;
      },
  
      get scrollContainer()
      {
          return this._scrollContainer;        
      },
  
      isScrolledToLastRow: function()
      {
          return this._scrollContainer.isScrolledToBottom();
      },
  
      scrollToLastRow: function()
      {
          this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight - this._scrollContainer.offsetHeight;
      },
  
      _positionResizers: function()
      {
          var headerTableColumns = this._headerTableColumnGroup.children;
          var numColumns = headerTableColumns.length;
          var left = 0;
          var previousResizer = null;
  
          // Make n - 1 resizers for n columns. 
          for (var i = 0; i < numColumns - 1; i++) {
              var resizer = this.resizers[i];
  
              if (!resizer) {
                  // This is the first call to updateWidth, so the resizers need
                  // to be created.
                  resizer = document.createElement("div");
                  resizer.addStyleClass("data-grid-resizer");
                  // This resizer is associated with the column to its right.
                  resizer.addEventListener("mousedown", this._startResizerDragging.bind(this), false);
                  this.element.appendChild(resizer);
                  this.resizers[i] = resizer;
              }
  
              // Get the width of the cell in the first (and only) row of the
              // header table in order to determine the width of the column, since
              // it is not possible to query a column for its width.
              left += this.headerTableBody.rows[0].cells[i].offsetWidth;
  
              var columnIsVisible = !this._headerTableColumnGroup.children[i].hidden;
              if (columnIsVisible) {
                  resizer.style.removeProperty("display");
                  resizer.style.left = left + "px";
                  resizer.leftNeighboringColumnID = i;
                  if (previousResizer)
                      previousResizer.rightNeighboringColumnID = i;
                  previousResizer = resizer;
              } else {
                  resizer.style.setProperty("display", "none");
                  resizer.leftNeighboringColumnID = 0;
                  resizer.rightNeighboringColumnID = 0;
              }
          }
          if (previousResizer)
              previousResizer.rightNeighboringColumnID = numColumns - 1;
      },
  
      addCreationNode: function(hasChildren)
      {
          if (this.creationNode)
              this.creationNode.makeNormal();
  
          var emptyData = {};
          for (var column in this.columns)
              emptyData[column] = '';
          this.creationNode = new WebInspector.CreationDataGridNode(emptyData, hasChildren);
          this.appendChild(this.creationNode);
      },
  
      appendChild: function(child)
      {
          this.insertChild(child, this.children.length);
      },
  
      insertChild: function(child, index)
      {
          if (!child)
              throw("insertChild: Node can't be undefined or null.");
          if (child.parent === this)
              throw("insertChild: Node is already a child of this node.");
  
          if (child.parent)
              child.parent.removeChild(child);
  
          this.children.splice(index, 0, child);
          this.hasChildren = true;
  
          child.parent = this;
          child.dataGrid = this.dataGrid;
          child._recalculateSiblings(index);
  
          delete child._depth;
          delete child._revealed;
          delete child._attached;
          child._shouldRefreshChildren = true;
  
          var current = child.children[0];
          while (current) {
              current.dataGrid = this.dataGrid;
              delete current._depth;
              delete current._revealed;
              delete current._attached;
              current._shouldRefreshChildren = true;
              current = current.traverseNextNode(false, child, true);
          }
  
          if (this.expanded)
              child._attach();
      },
  
      removeChild: function(child)
      {
          if (!child)
              throw("removeChild: Node can't be undefined or null.");
          if (child.parent !== this)
              throw("removeChild: Node is not a child of this node.");
  
          child.deselect();
          child._detach();
  
          this.children.remove(child, true);
  
          if (child.previousSibling)
              child.previousSibling.nextSibling = child.nextSibling;
          if (child.nextSibling)
              child.nextSibling.previousSibling = child.previousSibling;
  
          child.dataGrid = null;
          child.parent = null;
          child.nextSibling = null;
          child.previousSibling = null;
  
          if (this.children.length <= 0)
              this.hasChildren = false;
      },
  
      removeChildren: function()
      {
          for (var i = 0; i < this.children.length; ++i) {
              var child = this.children[i];
              child.deselect();
              child._detach();
  
              child.dataGrid = null;
              child.parent = null;
              child.nextSibling = null;
              child.previousSibling = null;
          }
  
          this.children = [];
          this.hasChildren = false;
      },
  
      removeChildrenRecursive: function()
      {
          var childrenToRemove = this.children;
  
          var child = this.children[0];
          while (child) {
              if (child.children.length)
                  childrenToRemove = childrenToRemove.concat(child.children);
              child = child.traverseNextNode(false, this, true);
          }
  
          for (var i = 0; i < childrenToRemove.length; ++i) {
              var child = childrenToRemove[i];
              child.deselect();
              child._detach();
  
              child.children = [];
              child.dataGrid = null;
              child.parent = null;
              child.nextSibling = null;
              child.previousSibling = null;
          }
  
          this.children = [];
      },
  
      sortNodes: function(comparator, reverseMode)
      {
          function comparatorWrapper(a, b)
          {
              if (a._dataGridNode._data.summaryRow)
                  return 1;
              if (b._dataGridNode._data.summaryRow)
                  return -1;
  
              var aDataGirdNode = a._dataGridNode;
              var bDataGirdNode = b._dataGridNode;
              return reverseMode ? comparator(bDataGirdNode, aDataGirdNode) : comparator(aDataGirdNode, bDataGirdNode);
          }
  
          var tbody = this.dataTableBody;
          var tbodyParent = tbody.parentElement;
          tbodyParent.removeChild(tbody);
  
          var childNodes = tbody.childNodes;
          var fillerRow = childNodes[childNodes.length - 1];
  
          var sortedRows = Array.prototype.slice.call(childNodes, 0, childNodes.length - 1);
          sortedRows.sort(comparatorWrapper);
          var sortedRowsLength = sortedRows.length;
  
          tbody.removeChildren();
          var previousSiblingNode = null;
          for (var i = 0; i < sortedRowsLength; ++i) {
              var row = sortedRows[i];
              var node = row._dataGridNode;
              node.previousSibling = previousSiblingNode;
              if (previousSiblingNode)
                  previousSiblingNode.nextSibling = node;
              tbody.appendChild(row);
              previousSiblingNode = node;
          }
          if (previousSiblingNode)
              previousSiblingNode.nextSibling = null;
  
          tbody.appendChild(fillerRow);
          tbodyParent.appendChild(tbody);
      },
  
      _keyDown: function(event)
      {
          if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing)
              return;
  
          var handled = false;
          var nextSelectedNode;
          if (event.keyIdentifier === "Up" && !event.altKey) {
              nextSelectedNode = this.selectedNode.traversePreviousNode(true);
              while (nextSelectedNode && !nextSelectedNode.selectable)
                  nextSelectedNode = nextSelectedNode.traversePreviousNode(!this.expandTreeNodesWhenArrowing);
              handled = nextSelectedNode ? true : false;
          } else if (event.keyIdentifier === "Down" && !event.altKey) {
              nextSelectedNode = this.selectedNode.traverseNextNode(true);
              while (nextSelectedNode && !nextSelectedNode.selectable)
                  nextSelectedNode = nextSelectedNode.traverseNextNode(!this.expandTreeNodesWhenArrowing);
              handled = nextSelectedNode ? true : false;
          } else if (event.keyIdentifier === "Left") {
              if (this.selectedNode.expanded) {
                  if (event.altKey)
                      this.selectedNode.collapseRecursively();
                  else
                      this.selectedNode.collapse();
                  handled = true;
              } else if (this.selectedNode.parent && !this.selectedNode.parent.root) {
                  handled = true;
                  if (this.selectedNode.parent.selectable) {
                      nextSelectedNode = this.selectedNode.parent;
                      handled = nextSelectedNode ? true : false;
                  } else if (this.selectedNode.parent)
                      this.selectedNode.parent.collapse();
              }
          } else if (event.keyIdentifier === "Right") {
              if (!this.selectedNode.revealed) {
                  this.selectedNode.reveal();
                  handled = true;
              } else if (this.selectedNode.hasChildren) {
                  handled = true;
                  if (this.selectedNode.expanded) {
                      nextSelectedNode = this.selectedNode.children[0];
                      handled = nextSelectedNode ? true : false;
                  } else {
                      if (event.altKey)
                          this.selectedNode.expandRecursively();
                      else
                          this.selectedNode.expand();
                  }
              }
          } else if (event.keyCode === 8 || event.keyCode === 46) {
              if (this._deleteCallback) {
                  handled = true;
                  this._deleteCallback(this.selectedNode);
              }
          } else if (isEnterKey(event)) {
              if (this._editCallback) {
                  handled = true;
                  // The first child of the selected element is the <td class="0-column">,
                  // and that's what we want to edit.
                  this._startEditing(this.selectedNode._element.children[0]);
              }
          }
  
          if (nextSelectedNode) {
              nextSelectedNode.reveal();
              nextSelectedNode.select();
          }
  
          if (handled) {
              event.preventDefault();
              event.stopPropagation();
          }
      },
  
      expand: function()
      {
          // This is the root, do nothing.
      },
  
      collapse: function()
      {
          // This is the root, do nothing.
      },
  
      reveal: function()
      {
          // This is the root, do nothing.
      },
  
      dataGridNodeFromNode: function(target)
      {
          var rowElement = target.enclosingNodeOrSelfWithNodeName("tr");
          return rowElement && rowElement._dataGridNode;
      },
  
      dataGridNodeFromPoint: function(x, y)
      {
          var node = this._dataTable.ownerDocument.elementFromPoint(x, y);
          var rowElement = node.enclosingNodeOrSelfWithNodeName("tr");
          return rowElement && rowElement._dataGridNode;
      },
  
      _clickInHeaderCell: function(event)
      {
          var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
          if (!cell || !cell.columnIdentifier || !cell.hasStyleClass("sortable"))
              return;
  
          var sortOrder = this.sortOrder;
  
          if (this._sortColumnCell)
              this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
  
          if (cell == this._sortColumnCell) {
              if (sortOrder === "ascending")
                  sortOrder = "descending";
              else
                  sortOrder = "ascending";
          }
  
          this._sortColumnCell = cell;
  
          cell.addStyleClass("sort-" + sortOrder);
  
          this.dispatchEventToListeners("sorting changed");
      },
  
      markColumnAsSortedBy: function(columnIdentifier, sortOrder)
      {
          if (this._sortColumnCell)
              this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
          this._sortColumnCell = this._headerTableHeaders[columnIdentifier];
          this._sortColumnCell.addStyleClass("sort-" + sortOrder);
      },
  
      headerTableHeader: function(columnIdentifier)
      {
          return this._headerTableHeaders[columnIdentifier];
      },
  
      _mouseDownInDataTable: function(event)
      {
          var gridNode = this.dataGridNodeFromNode(event.target);
          if (!gridNode || !gridNode.selectable)
              return;
  
          if (gridNode.isEventWithinDisclosureTriangle(event))
              return;
  
          if (event.metaKey) {
              if (gridNode.selected)
                  gridNode.deselect();
              else
                  gridNode.select();
          } else
              gridNode.select();
      },
      
      _contextMenuInDataTable: function(event)
      {
          var gridNode = this.dataGridNodeFromNode(event.target);
          if (!gridNode || !gridNode.selectable)
              return;
          
          if (gridNode.isEventWithinDisclosureTriangle(event))
              return;
        
          var contextMenu = new WebInspector.ContextMenu();
          
          // FIXME: Use the column names for Editing, instead of just "Edit".
          if (this.dataGrid._editCallback) {
              if (gridNode === this.creationNode)
                  contextMenu.appendItem(WebInspector.UIString("Add New"), this._startEditing.bind(this, event.target));
              else
                  contextMenu.appendItem(WebInspector.UIString("Edit"), this._startEditing.bind(this, event.target));
          }
          if (this.dataGrid._deleteCallback && gridNode !== this.creationNode)
              contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode));
          
          contextMenu.show(event);
      },
  
      _clickInDataTable: function(event)
      {
          var gridNode = this.dataGridNodeFromNode(event.target);
          if (!gridNode || !gridNode.hasChildren)
              return;
  
          if (!gridNode.isEventWithinDisclosureTriangle(event))
              return;
  
          if (gridNode.expanded) {
              if (event.altKey)
                  gridNode.collapseRecursively();
              else
                  gridNode.collapse();
          } else {
              if (event.altKey)
                  gridNode.expandRecursively();
              else
                  gridNode.expand();
          }
      },
      
      _startResizerDragging: function(event)
      {
          this.currentResizer = event.target;
          if (!this.currentResizer.rightNeighboringColumnID)
              return;
          WebInspector.elementDragStart(this.lastResizer, this._resizerDragging.bind(this),
              this._endResizerDragging.bind(this), event, "col-resize");
      },
      
      _resizerDragging: function(event)
      {
          var resizer = this.currentResizer;
          if (!resizer)
              return;
          
          // Constrain the dragpoint to be within the containing div of the
          // datagrid.
          var dragPoint = event.clientX - this.element.totalOffsetLeft;
          // Constrain the dragpoint to be within the space made up by the
          // column directly to the left and the column directly to the right.
          var leftEdgeOfPreviousColumn = 0;
          var firstRowCells = this.headerTableBody.rows[0].cells;
          for (var i = 0; i < resizer.leftNeighboringColumnID; i++)
              leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth;
              
          var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[resizer.leftNeighboringColumnID].offsetWidth + firstRowCells[resizer.rightNeighboringColumnID].offsetWidth;
  
          // Give each column some padding so that they don't disappear.
          var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding;
          var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding;
  
          dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum);
  
          resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px";
  
          var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / this._dataTable.offsetWidth) * 100) + "%";
          this._headerTableColumnGroup.children[resizer.leftNeighboringColumnID].style.width = percentLeftColumn;
          this._dataTableColumnGroup.children[resizer.leftNeighboringColumnID].style.width = percentLeftColumn;
  
          var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / this._dataTable.offsetWidth) * 100) + "%";
          this._headerTableColumnGroup.children[resizer.rightNeighboringColumnID].style.width =  percentRightColumn;
          this._dataTableColumnGroup.children[resizer.rightNeighboringColumnID].style.width = percentRightColumn;
  
          this._positionResizers();
          event.preventDefault();
          this.dispatchEventToListeners("width changed");
      },
      
      _endResizerDragging: function(event)
      {
          WebInspector.elementDragEnd(event);
          this.currentResizer = null;
          this.dispatchEventToListeners("width changed");
      },
      
      ColumnResizePadding: 10,
      
      CenterResizerOverBorderAdjustment: 3,
  }
  
  WebInspector.DataGrid.prototype.__proto__ = WebInspector.Object.prototype;
  
  WebInspector.DataGridNode = function(data, hasChildren)
  {
      this._expanded = false;
      this._selected = false;
      this._shouldRefreshChildren = true;
      this._data = data || {};
      this.hasChildren = hasChildren || false;
      this.children = [];
      this.dataGrid = null;
      this.parent = null;
      this.previousSibling = null;
      this.nextSibling = null;
      this.disclosureToggleWidth = 10;
  }
  
  WebInspector.DataGridNode.prototype = {
      selectable: true,
  
      get element()
      {
          if (this._element)
              return this._element;
  
          if (!this.dataGrid)
              return null;
  
          this._element = document.createElement("tr");
          this._element._dataGridNode = this;
  
          if (this.hasChildren)
              this._element.addStyleClass("parent");
          if (this.expanded)
              this._element.addStyleClass("expanded");
          if (this.selected)
              this._element.addStyleClass("selected");
          if (this.revealed)
              this._element.addStyleClass("revealed");
  
          this.createCells();
          return this._element;
      },
  
      createCells: function()
      {
          for (var columnIdentifier in this.dataGrid.columns) {
              var cell = this.createCell(columnIdentifier);
              this._element.appendChild(cell);
          }
      },
  
      get data()
      {
          return this._data;
      },
  
      set data(x)
      {
          this._data = x || {};
          this.refresh();
      },
  
      get revealed()
      {
          if ("_revealed" in this)
              return this._revealed;
  
          var currentAncestor = this.parent;
          while (currentAncestor && !currentAncestor.root) {
              if (!currentAncestor.expanded) {
                  this._revealed = false;
                  return false;
              }
  
              currentAncestor = currentAncestor.parent;
          }
  
          this._revealed = true;
          return true;
      },
  
      set hasChildren(x)
      {
          if (this._hasChildren === x)
              return;
  
          this._hasChildren = x;
  
          if (!this._element)
              return;
  
          if (this._hasChildren)
          {
              this._element.addStyleClass("parent");
              if (this.expanded)
                  this._element.addStyleClass("expanded");
          }
          else
          {
              this._element.removeStyleClass("parent");
              this._element.removeStyleClass("expanded");
          }
      },
  
      get hasChildren()
      {
          return this._hasChildren;
      },
  
      set revealed(x)
      {
          if (this._revealed === x)
              return;
  
          this._revealed = x;
  
          if (this._element) {
              if (this._revealed)
                  this._element.addStyleClass("revealed");
              else
                  this._element.removeStyleClass("revealed");
          }
  
          for (var i = 0; i < this.children.length; ++i)
              this.children[i].revealed = x && this.expanded;
      },
  
      get depth()
      {
          if ("_depth" in this)
              return this._depth;
          if (this.parent && !this.parent.root)
              this._depth = this.parent.depth + 1;
          else
              this._depth = 0;
          return this._depth;
      },
  
      get shouldRefreshChildren()
      {
          return this._shouldRefreshChildren;
      },
  
      set shouldRefreshChildren(x)
      {
          this._shouldRefreshChildren = x;
          if (x && this.expanded)
              this.expand();
      },
  
      get selected()
      {
          return this._selected;
      },
  
      set selected(x)
      {
          if (x)
              this.select();
          else
              this.deselect();
      },
  
      get expanded()
      {
          return this._expanded;
      },
  
      set expanded(x)
      {
          if (x)
              this.expand();
          else
              this.collapse();
      },
  
      refresh: function()
      {
          if (!this._element || !this.dataGrid)
              return;
  
          this._element.removeChildren();
          this.createCells();
      },
  
      createCell: function(columnIdentifier)
      {
          var cell = document.createElement("td");
          cell.className = columnIdentifier + "-column";
  
          var alignment = this.dataGrid.aligned[columnIdentifier];
          if (alignment)
              cell.addStyleClass(alignment);
  
          var div = document.createElement("div");
          div.textContent = this.data[columnIdentifier];
          cell.appendChild(div);
  
          if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
              cell.addStyleClass("disclosure");
              if (this.depth)
                  cell.style.setProperty("padding-left", (this.depth * this.dataGrid.indentWidth) + "px");
          }
  
          return cell;
      },
  
      // Share these functions with DataGrid. They are written to work with a DataGridNode this object.
      appendChild: WebInspector.DataGrid.prototype.appendChild,
      insertChild: WebInspector.DataGrid.prototype.insertChild,
      removeChild: WebInspector.DataGrid.prototype.removeChild,
      removeChildren: WebInspector.DataGrid.prototype.removeChildren,
      removeChildrenRecursive: WebInspector.DataGrid.prototype.removeChildrenRecursive,
  
      _recalculateSiblings: function(myIndex)
      {
          if (!this.parent)
              return;
  
          var previousChild = (myIndex > 0 ? this.parent.children[myIndex - 1] : null);
  
          if (previousChild) {
              previousChild.nextSibling = this;
              this.previousSibling = previousChild;
          } else
              this.previousSibling = null;
  
          var nextChild = this.parent.children[myIndex + 1];
  
          if (nextChild) {
              nextChild.previousSibling = this;
              this.nextSibling = nextChild;
          } else
              this.nextSibling = null;
      },
  
      collapse: function()
      {
          if (this._element)
              this._element.removeStyleClass("expanded");
  
          this._expanded = false;
  
          for (var i = 0; i < this.children.length; ++i)
              this.children[i].revealed = false;
  
          this.dispatchEventToListeners("collapsed");
      },
  
      collapseRecursively: function()
      {
          var item = this;
          while (item) {
              if (item.expanded)
                  item.collapse();
              item = item.traverseNextNode(false, this, true);
          }
      },
  
      expand: function()
      {
          if (!this.hasChildren || this.expanded)
              return;
  
          if (this.revealed && !this._shouldRefreshChildren)
              for (var i = 0; i < this.children.length; ++i)
                  this.children[i].revealed = true;
  
          if (this._shouldRefreshChildren) {
              for (var i = 0; i < this.children.length; ++i)
                  this.children[i]._detach();
  
              this.dispatchEventToListeners("populate");
  
              if (this._attached) {
                  for (var i = 0; i < this.children.length; ++i) {
                      var child = this.children[i];
                      if (this.revealed)
                          child.revealed = true;
                      child._attach();
                  }
              }
  
              delete this._shouldRefreshChildren;
          }
  
          if (this._element)
              this._element.addStyleClass("expanded");
  
          this._expanded = true;
  
          this.dispatchEventToListeners("expanded");
      },
  
      expandRecursively: function()
      {
          var item = this;
          while (item) {
              item.expand();
              item = item.traverseNextNode(false, this);
          }
      },
  
      reveal: function()
      {
          var currentAncestor = this.parent;
          while (currentAncestor && !currentAncestor.root) {
              if (!currentAncestor.expanded)
                  currentAncestor.expand();
              currentAncestor = currentAncestor.parent;
          }
  
          this.element.scrollIntoViewIfNeeded(false);
  
          this.dispatchEventToListeners("revealed");
      },
  
      select: function(supressSelectedEvent)
      {
          if (!this.dataGrid || !this.selectable || this.selected)
              return;
  
          if (this.dataGrid.selectedNode)
              this.dataGrid.selectedNode.deselect();
  
          this._selected = true;
          this.dataGrid.selectedNode = this;
  
          if (this._element)
              this._element.addStyleClass("selected");
  
          if (!supressSelectedEvent)
              this.dispatchEventToListeners("selected");
      },
  
      deselect: function(supressDeselectedEvent)
      {
          if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
              return;
  
          this._selected = false;
          this.dataGrid.selectedNode = null;
  
          if (this._element)
              this._element.removeStyleClass("selected");
  
          if (!supressDeselectedEvent)
              this.dispatchEventToListeners("deselected");
      },
  
      traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info)
      {
          if (!dontPopulate && this.hasChildren)
              this.dispatchEventToListeners("populate");
  
          if (info)
              info.depthChange = 0;
  
          var node = (!skipHidden || this.revealed) ? this.children[0] : null;
          if (node && (!skipHidden || this.expanded)) {
              if (info)
                  info.depthChange = 1;
              return node;
          }
  
          if (this === stayWithin)
              return null;
  
          node = (!skipHidden || this.revealed) ? this.nextSibling : null;
          if (node)
              return node;
  
          node = this;
          while (node && !node.root && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) {
              if (info)
                  info.depthChange -= 1;
              node = node.parent;
          }
  
          if (!node)
              return null;
  
          return (!skipHidden || node.revealed) ? node.nextSibling : null;
      },
  
      traversePreviousNode: function(skipHidden, dontPopulate)
      {
          var node = (!skipHidden || this.revealed) ? this.previousSibling : null;
          if (!dontPopulate && node && node.hasChildren)
              node.dispatchEventToListeners("populate");
  
          while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null)) {
              if (!dontPopulate && node.hasChildren)
                  node.dispatchEventToListeners("populate");
              node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null);
          }
  
          if (node)
              return node;
  
          if (!this.parent || this.parent.root)
              return null;
  
          return this.parent;
      },
  
      isEventWithinDisclosureTriangle: function(event)
      {
          if (!this.hasChildren)
              return false;
          var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
          if (!cell.hasStyleClass("disclosure"))
              return false;
          var computedLeftPadding = window.getComputedStyle(cell).getPropertyCSSValue("padding-left").getFloatValue(CSSPrimitiveValue.CSS_PX);
          var left = cell.totalOffsetLeft + computedLeftPadding;
          return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth;
      },
  
      _attach: function()
      {
          if (!this.dataGrid || this._attached)
              return;
  
          this._attached = true;
  
          var nextNode = null;
          var previousNode = this.traversePreviousNode(true, true);
          if (previousNode && previousNode.element.parentNode && previousNode.element.nextSibling)
              var nextNode = previousNode.element.nextSibling;
          if (!nextNode)
              nextNode = this.dataGrid.dataTableBody.lastChild;
          this.dataGrid.dataTableBody.insertBefore(this.element, nextNode);
  
          if (this.expanded)
              for (var i = 0; i < this.children.length; ++i)
                  this.children[i]._attach();
      },
  
      _detach: function()
      {
          if (!this._attached)
              return;
  
          this._attached = false;
  
          if (this._element && this._element.parentNode)
              this._element.parentNode.removeChild(this._element);
  
          for (var i = 0; i < this.children.length; ++i)
              this.children[i]._detach();
      },
  
      savePosition: function()
      {
          if (this._savedPosition)
              return;
  
          if (!this.parent)
              throw("savePosition: Node must have a parent.");
          this._savedPosition = {
              parent: this.parent,
              index: this.parent.children.indexOf(this)
          };
      },
  
      restorePosition: function()
      {
          if (!this._savedPosition)
              return;
  
          if (this.parent !== this._savedPosition.parent)
              this._savedPosition.parent.insertChild(this, this._savedPosition.index);
  
          delete this._savedPosition;
      }
  }
  
  WebInspector.DataGridNode.prototype.__proto__ = WebInspector.Object.prototype;
  
  WebInspector.CreationDataGridNode = function(data, hasChildren)
  {
      WebInspector.DataGridNode.call(this, data, hasChildren);
      this.isCreationNode = true;
  }
  
  WebInspector.CreationDataGridNode.prototype = {
      makeNormal: function()
      {
          delete this.isCreationNode;
          delete this.makeNormal;
      }
  }
  
  WebInspector.CreationDataGridNode.prototype.__proto__ = WebInspector.DataGridNode.prototype;
  