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