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