Organize WebInspectorUI/UserInterface into sub-directories.
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / DataGrid.js
1 /*
2  * Copyright (C) 2008, 2013, 2014 Apple Inc. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *        notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *        notice, this list of conditions and the following disclaimer in the
11  *        documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.         IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.DataGrid = function(columnsData, editCallback, deleteCallback)
27 {
28     this.columns = new Map;
29     this.orderedColumns = [];
30
31     this.children = [];
32     this.selectedNode = null;
33     this.expandNodesWhenArrowing = false;
34     this.root = true;
35     this.hasChildren = false;
36     this.expanded = true;
37     this.revealed = true;
38     this.selected = false;
39     this.dataGrid = this;
40     this.indentWidth = 15;
41     this.resizerElements = [];
42     this._columnWidthsInitialized = false;
43
44     this.element = document.createElement("div");
45     this.element.className = "data-grid";
46     this.element.tabIndex = 0;
47     this.element.addEventListener("keydown", this._keyDown.bind(this), false);
48     this.element.copyHandler = this;
49
50     this._headerTableElement = document.createElement("table");
51     this._headerTableElement.className = "header";
52     this._headerTableColumnGroupElement = this._headerTableElement.createChild("colgroup");
53     this._headerTableBodyElement = this._headerTableElement.createChild("tbody");
54     this._headerTableRowElement = this._headerTableBodyElement.createChild("tr");
55     this._headerTableCellElements = new Map;
56
57     this._scrollContainerElement = document.createElement("div");
58     this._scrollContainerElement.className = "data-container";
59
60     this._dataTableElement = this._scrollContainerElement.createChild("table");
61     this._dataTableElement.className = "data";
62
63     this._dataTableElement.addEventListener("mousedown", this._mouseDownInDataTable.bind(this));
64     this._dataTableElement.addEventListener("click", this._clickInDataTable.bind(this));
65     this._dataTableElement.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true);
66
67     // FIXME: Add a createCallback which is different from editCallback and has different
68     // behavior when creating a new node.
69     if (editCallback) {
70         this._dataTableElement.addEventListener("dblclick", this._ondblclick.bind(this), false);
71         this._editCallback = editCallback;
72     }
73     if (deleteCallback)
74         this._deleteCallback = deleteCallback;
75
76     this._dataTableColumnGroupElement = this._headerTableColumnGroupElement.cloneNode(true);
77     this._dataTableElement.appendChild(this._dataTableColumnGroupElement);
78
79     // This element is used by DataGridNodes to manipulate table rows and cells.
80     this.dataTableBodyElement = this._dataTableElement.createChild("tbody");
81     this._fillerRowElement = this.dataTableBodyElement.createChild("tr");
82     this._fillerRowElement.className = "filler";
83
84     this.element.appendChild(this._headerTableElement);
85     this.element.appendChild(this._scrollContainerElement);
86
87     for (var columnIdentifier in columnsData)
88         this.insertColumn(columnIdentifier, columnsData[columnIdentifier]);
89
90     this._generateSortIndicatorImagesIfNeeded();
91 }
92
93 WebInspector.DataGrid.Event = {
94     DidLayout: "datagrid-did-layout",
95     SortChanged: "datagrid-sort-changed",
96     SelectedNodeChanged: "datagrid-selected-node-changed",
97     ExpandedNode: "datagrid-expanded-node",
98     CollapsedNode: "datagrid-collapsed-node"
99 };
100
101 /**
102  * @param {Array.<string>} columnNames
103  * @param {Array.<string>} values
104  */
105 WebInspector.DataGrid.createSortableDataGrid = function(columnNames, values)
106 {
107     var numColumns = columnNames.length;
108     if (!numColumns)
109         return null;
110
111     var columnsData = {};
112
113     for (var columnName of columnNames) {
114         var column = {};
115         column.width = columnName.length;
116         column.title = columnName;
117         column.sortable = true;
118
119         columnsData[columnName] = column;
120     }
121
122     var dataGrid = new WebInspector.DataGrid(columnsData);
123     for (var i = 0; i < values.length / numColumns; ++i) {
124         var data = {};
125         for (var j = 0; j < columnNames.length; ++j)
126             data[columnNames[j]] = values[numColumns * i + j];
127
128         var node = new WebInspector.DataGridNode(data, false);
129         node.selectable = false;
130         dataGrid.appendChild(node);
131     }
132
133     function sortDataGrid()
134     {
135         var sortColumnIdentifier = dataGrid.sortColumnIdentifier;
136         var sortAscending = dataGrid.sortOrder === "ascending" ? 1 : -1;
137
138         for (var node of dataGrid.children) {
139             if (isNaN(Number(node.data[sortColumnIdentifier] || "")))
140                 columnIsNumeric = false;
141         }
142
143         function comparator(dataGridNode1, dataGridNode2)
144         {
145             var item1 = dataGridNode1.data[sortColumnIdentifier] || "";
146             var item2 = dataGridNode2.data[sortColumnIdentifier] || "";
147
148             var comparison;
149             if (columnIsNumeric) {
150                 // Sort numbers based on comparing their values rather than a lexicographical comparison.
151                 var number1 = parseFloat(item1);
152                 var number2 = parseFloat(item2);
153                 comparison = number1 < number2 ? -1 : (number1 > number2 ? 1 : 0);
154             } else
155                 comparison = item1 < item2 ? -1 : (item1 > item2 ? 1 : 0);
156
157             return sortDirection * comparison;
158         }
159
160         dataGrid.sortNodes(comparator);
161     }
162
163     dataGrid.addEventListener(WebInspector.DataGrid.Event.SortChanged, sortDataGrid, this);
164     return dataGrid;
165 }
166
167 WebInspector.DataGrid.prototype = {
168     get refreshCallback()
169     {
170         return this._refreshCallback;
171     },
172
173     set refreshCallback(refreshCallback)
174     {
175         this._refreshCallback = refreshCallback;
176     },
177
178     _ondblclick: function(event)
179     {
180         if (this._editing || this._editingNode)
181             return;
182
183         this._startEditing(event.target);
184     },
185
186     _startEditingNodeAtColumnIndex: function(node, columnIndex)
187     {
188         console.assert(node, "Invalid argument: must provide DataGridNode to edit.");
189
190         this._editing = true;
191         this._editingNode = node;
192         this._editingNode.select();
193
194         var element = this._editingNode._element.children[columnIndex];
195         WebInspector.startEditing(element, this._startEditingConfig(element));
196         window.getSelection().setBaseAndExtent(element, 0, element, 1);
197     },
198
199     _startEditing: function(target)
200     {
201         var element = target.enclosingNodeOrSelfWithNodeName("td");
202         if (!element)
203             return;
204
205         this._editingNode = this.dataGridNodeFromNode(target);
206         if (!this._editingNode) {
207             if (!this.placeholderNode)
208                 return;
209             this._editingNode = this.placeholderNode;
210         }
211
212         // Force editing the 1st column when editing the placeholder node
213         if (this._editingNode.isPlaceholderNode)
214             return this._startEditingNodeAtColumnIndex(this._editingNode, 0);
215
216         this._editing = true;
217         WebInspector.startEditing(element, this._startEditingConfig(element));
218
219         window.getSelection().setBaseAndExtent(element, 0, element, 1);
220     },
221
222     _startEditingConfig: function(element)
223     {
224         return new WebInspector.EditingConfig(this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent);
225     },
226
227     _editingCommitted: function(element, newText, oldText, context, moveDirection)
228     {
229         var columnIdentifier = element.__columnIdentifier;
230         var columnIndex = this.orderedColumns.indexOf(columnIdentifier);
231
232         var textBeforeEditing = this._editingNode.data[columnIdentifier] || "";
233         var currentEditingNode = this._editingNode;
234
235         // Returns an object with the next node and column index to edit, and whether it
236         // is an appropriate time to re-sort the table rows. When editing, we want to
237         // postpone sorting until we switch rows or wrap around a row.
238         function determineNextCell(valueDidChange) {
239             if (moveDirection === "forward") {
240                 if (columnIndex < this.orderedColumns.length - 1)
241                     return {shouldSort: false, editingNode: currentEditingNode, columnIndex: columnIndex + 1};
242
243                 // Continue by editing the first column of the next row if it exists.
244                 var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true);
245                 return {shouldSort: true, editingNode: nextDataGridNode || currentEditingNode, columnIndex: 0};
246             }
247
248             if (moveDirection === "backward") {
249                 if (columnIndex > 0)
250                     return {shouldSort: false, editingNode: currentEditingNode, columnIndex: columnIndex - 1};
251
252                 var previousDataGridNode = currentEditingNode.traversePreviousNode(true, null, true);
253                 return {shouldSort: true, editingNode: previousDataGridNode || currentEditingNode, columnIndex: this.orderedColumns.length - 1};
254             }
255
256             // If we are not moving in any direction, then sort but don't move.
257             return {shouldSort: true, editingNode: currentEditingNode, columnIndex: columnIndex};
258         }
259
260         function moveToNextCell(valueDidChange) {
261             var moveCommand = determineNextCell.call(this, valueDidChange);
262             if (moveCommand.shouldSort && this._sortAfterEditingCallback) {
263                 this._sortAfterEditingCallback();
264                 delete this._sortAfterEditingCallback;
265             }
266             this._startEditingNodeAtColumnIndex(moveCommand.editingNode, moveCommand.columnIndex);
267         }
268
269         this._editingCancelled(element);
270
271         // Update table's data model, and delegate to the callback to update other models.
272         currentEditingNode.data[columnIdentifier] = newText.trim();
273         this._editCallback(currentEditingNode, columnIdentifier, textBeforeEditing, newText, moveDirection);
274
275         var textDidChange = textBeforeEditing.trim() !== newText.trim();
276         moveToNextCell.call(this, textDidChange);
277     },
278
279     _editingCancelled: function(element)
280     {
281         console.assert(this._editingNode.element === element.enclosingNodeOrSelfWithNodeName("tr"));
282         delete this._editing;
283         this._editingNode = null;
284     },
285
286     get sortColumnIdentifier()
287     {
288         return this._sortColumnCell ? this._sortColumnCell.columnIdentifier : null;
289     },
290
291     get sortOrder()
292     {
293         if (!this._sortColumnCell || this._sortColumnCell.classList.contains("sort-ascending"))
294             return "ascending";
295         if (this._sortColumnCell.classList.contains("sort-descending"))
296             return "descending";
297         return null;
298     },
299
300     autoSizeColumns: function(minPercent, maxPercent, maxDescentLevel)
301     {
302         if (minPercent)
303             minPercent = Math.min(minPercent, Math.floor(100 / this.orderedColumns.length));
304         var widths = {};
305         // For the first width approximation, use the character length of column titles.
306         for (var [identifier, column] of this.columns)
307             widths[identifier] = column.get("title", "").length;
308
309         // Now approximate the width of each column as max(title, cells).
310         var children = maxDescentLevel ? this._enumerateChildren(this, [], maxDescentLevel + 1) : this.children;
311         for (var node of children) {
312             for (var identifier of this.columns.keys()) {
313                 var text = node.data[identifier] || "";
314                 if (text.length > widths[identifier])
315                     widths[identifier] = text.length;
316             }
317         }
318
319         var totalColumnWidths = 0;
320         for (var identifier of this.columns.keys())
321             totalColumnWidths += widths[identifier];
322
323         // Compute percentages and clamp desired widths to min and max widths.
324         var recoupPercent = 0;
325         for (var identifier of this.columns.keys()) {
326             var width = Math.round(100 * widths[identifier] / totalColumnWidths);
327             if (minPercent && width < minPercent) {
328                 recoupPercent += (minPercent - width);
329                 width = minPercent;
330             } else if (maxPercent && width > maxPercent) {
331                 recoupPercent -= (width - maxPercent);
332                 width = maxPercent;
333             }
334             widths[identifier] = width;
335         }
336
337         // If we assigned too much width due to the above, reduce column widths.
338         while (minPercent && recoupPercent > 0) {
339             for (var identifier of this.columns.keys()) {
340                 if (widths[identifier] > minPercent) {
341                     --widths[identifier];
342                     --recoupPercent;
343                     if (!recoupPercent)
344                         break;
345                 }
346             }
347         }
348
349         // If extra width remains after clamping widths, expand column widths.
350         while (maxPercent && recoupPercent < 0) {
351             for (var identifier of this.columns.keys()) {
352                 if (widths[identifier] < maxPercent) {
353                     ++widths[identifier];
354                     ++recoupPercent;
355                     if (!recoupPercent)
356                         break;
357                 }
358             }
359         }
360
361         for (var [identifier, column] of this.columns)
362             column.get("element").style.width = widths[identifier] + "%";
363         this._columnWidthsInitialized = false;
364         this.updateLayout();
365     },
366
367     insertColumn: function(columnIdentifier, columnData, insertionIndex) {
368         if (typeof insertionIndex === "undefined")
369             insertionIndex = this.orderedColumns.length;
370         insertionIndex = Number.constrain(insertionIndex, 0, this.orderedColumns.length);
371
372         var listeners = new WebInspector.EventListenerSet(this, "DataGrid column DOM listeners");
373
374         // Copy configuration properties instead of keeping a reference to the passed-in object.
375         var column = new Map;
376         for (var propertyName in columnData)
377             column.set(propertyName, columnData[propertyName]);
378
379         column.set("listeners", listeners);
380         column.set("ordinal", insertionIndex);
381         column.set("columnIdentifier", columnIdentifier);
382         this.orderedColumns.splice(insertionIndex, 0, columnIdentifier);
383
384         for (var [identifier, existingColumn] of this.columns) {
385             var ordinal = existingColumn.get("ordinal");
386             if (ordinal >= insertionIndex) // Also adjust the "old" column at insertion index.
387                 existingColumn.set("ordinal", ordinal + 1);
388         }
389         this.columns.set(columnIdentifier, column);
390
391         if (column.has("disclosure"))
392             this.disclosureColumnIdentifier = columnIdentifier;
393
394         var headerColumnElement = document.createElement("col");
395         if (column.has("width"))
396             headerColumnElement.style.width = column.get("width");
397         column.set("element", headerColumnElement);
398         var referenceElement = this._headerTableColumnGroupElement.children[insertionIndex];
399         this._headerTableColumnGroupElement.insertBefore(headerColumnElement, referenceElement);
400
401         var headerCellElement = document.createElement("th");
402         headerCellElement.className = columnIdentifier + "-column";
403         headerCellElement.columnIdentifier = columnIdentifier;
404         if (column.has("aligned"))
405             headerCellElement.classList.add(column.get("aligned"));
406         this._headerTableCellElements.set(columnIdentifier, headerCellElement);
407         var referenceElement = this._headerTableRowElement.children[insertionIndex];
408         this._headerTableRowElement.insertBefore(headerCellElement, referenceElement);
409
410         var div = headerCellElement.createChild("div");
411         if (column.has("titleDOMFragment"))
412             div.appendChild(column.get("titleDOMFragment"));
413         else
414             div.textContent = column.get("title", "");
415
416         if (column.has("sort")) {
417             headerCellElement.classList.add("sort-" + column.get("sort"));
418             this._sortColumnCell = headerCellElement;
419         }
420
421         if (column.has("sortable")) {
422             listeners.register(headerCellElement, "click", this._clickInHeaderCell, false);
423             headerCellElement.classList.add("sortable");
424         }
425
426         if (column.has("group"))
427             headerCellElement.classList.add("column-group-" + column.get("group"));
428
429         if (column.has("collapsesGroup")) {
430             console.assert(column.get("group") !== column.get("collapsesGroup"));
431
432             var dividerElement = headerCellElement.createChild("div");
433             dividerElement.className = "divider";
434
435             var collapseDiv = headerCellElement.createChild("div");
436             collapseDiv.className = "collapser-button";
437             collapseDiv.title = this._collapserButtonCollapseColumnsToolTip();
438             listeners.register(collapseDiv, "mouseover", this._mouseoverColumnCollapser);
439             listeners.register(collapseDiv, "mouseout", this._mouseoutColumnCollapser);
440             listeners.register(collapseDiv, "click", this._clickInColumnCollapser);
441
442             headerCellElement.collapsesGroup = column.get("collapsesGroup");
443             headerCellElement.classList.add("collapser");
444         }
445
446         this._headerTableColumnGroupElement.span = this.orderedColumns.length;
447
448         var dataColumnElement = headerColumnElement.cloneNode();
449         var referenceElement = this._dataTableColumnGroupElement.children[insertionIndex];
450         this._dataTableColumnGroupElement.insertBefore(dataColumnElement, referenceElement);
451         column.set("bodyElement", dataColumnElement);
452
453         var fillerCellElement = document.createElement("td");
454         fillerCellElement.className = columnIdentifier + "-column";
455         fillerCellElement.__columnIdentifier = columnIdentifier;
456         if (column.has("group"))
457             fillerCellElement.classList.add("column-group-" + column.get("group"));
458         var referenceElement = this._fillerRowElement.children[insertionIndex];
459         this._fillerRowElement.insertBefore(fillerCellElement, referenceElement);
460
461         listeners.install();
462
463         if (column.has("hidden"))
464             this._hideColumn(columnIdentifier);
465     },
466
467     removeColumn: function(columnIdentifier)
468     {
469         console.assert(this.columns.has(columnIdentifier));
470         var removedColumn = this.columns.get(columnIdentifier);
471         this.columns.delete(columnIdentifier);
472         this.orderedColumns.splice(this.orderedColumns.indexOf(columnIdentifier), 1);
473
474         var removedOrdinal = removedColumn.get("ordinal");
475         for (var [identifier, column] of this.columns) {
476             var ordinal = column.get("ordinal");
477             if (ordinal > removedOrdinal)
478                 column.set("ordinal", ordinal - 1);
479         }
480
481         removedColumn.get("listeners").uninstall(true);
482
483         if (removedColumn.has("disclosure"))
484             delete this.disclosureColumnIdentifier;
485
486         if (removedColumn.has("sort"))
487             delete this._sortColumnCell;
488
489         this._headerTableCellElements.delete(columnIdentifier);
490         this._headerTableRowElement.children[removedOrdinal].remove();
491         this._headerTableColumnGroupElement.children[removedOrdinal].remove();
492         this._dataTableColumnGroupElement.children[removedOrdinal].remove();
493         this._fillerRowElement.children[removedOrdinal].remove();
494
495         this._headerTableColumnGroupElement.span = this.orderedColumns.length;
496
497         for (var child of this.children)
498             child.refresh();
499     },
500
501     _enumerateChildren: function(rootNode, result, maxLevel)
502     {
503         if (!rootNode.root)
504             result.push(rootNode);
505         if (!maxLevel)
506             return;
507         for (var i = 0; i < rootNode.children.length; ++i)
508             this._enumerateChildren(rootNode.children[i], result, maxLevel - 1);
509         return result;
510     },
511
512     // Updates the widths of the table, including the positions of the column
513     // resizers.
514     //
515     // IMPORTANT: This function MUST be called once after the element of the
516     // DataGrid is attached to its parent element and every subsequent time the
517     // width of the parent element is changed in order to make it possible to
518     // resize the columns.
519     //
520     // If this function is not called after the DataGrid is attached to its
521     // parent element, then the DataGrid's columns will not be resizable.
522     updateLayout: function()
523     {
524         // Do not attempt to use offsetes if we're not attached to the document tree yet.
525         if (!this._columnWidthsInitialized && this.element.offsetWidth) {
526             // Give all the columns initial widths now so that during a resize,
527             // when the two columns that get resized get a percent value for
528             // their widths, all the other columns already have percent values
529             // for their widths.
530             var headerTableColumnElements = this._headerTableColumnGroupElement.children;
531             var tableWidth = this._dataTableElement.offsetWidth;
532             var numColumns = headerTableColumnElements.length;
533             for (var i = 0; i < numColumns; i++) {
534                 var headerCellElement = this._headerTableBodyElement.rows[0].cells[i]
535                 if (this._isColumnVisible(headerCellElement.columnIdentifier)) {
536                     var columnWidth = headerCellElement.offsetWidth;
537                     var percentWidth = ((columnWidth / tableWidth) * 100) + "%";
538                     this._headerTableColumnGroupElement.children[i].style.width = percentWidth;
539                     this._dataTableColumnGroupElement.children[i].style.width = percentWidth;
540                 } else {
541                     this._headerTableColumnGroupElement.children[i].style.width = 0;
542                     this._dataTableColumnGroupElement.children[i].style.width = 0;
543                 }
544             }
545
546             this._columnWidthsInitialized = true;
547         }
548
549         this._positionResizerElements();
550         this.dispatchEventToListeners(WebInspector.DataGrid.Event.DidLayout);
551     },
552
553     columnWidthsMap: function()
554     {
555         var result = {};
556         for (var [identifier, column] of this.columns) {
557             var width = this._headerTableColumnGroupElement.children[column.get("ordinal")].style.width;
558             result[columnIdentifier] = parseFloat(width);
559         }
560         return result;
561     },
562
563     applyColumnWidthsMap: function(columnWidthsMap)
564     {
565         for (var [identifier, column] of this.columns) {
566             var width = (columnWidthsMap[identifier] || 0) + "%";
567             var ordinal = column.get("ordinal");
568             this._headerTableColumnGroupElement.children[ordinal].style.width = width;
569             this._dataTableColumnGroupElement.children[ordinal].style.width = width;
570         }
571
572         this.updateLayout();
573     },
574
575     _isColumnVisible: function(columnIdentifier)
576     {
577         return !this.columns.get(columnIdentifier).has("hidden");
578     },
579
580     _showColumn: function(columnIdentifier)
581     {
582         this.columns.get(columnIdentifier).delete("hidden");
583     },
584
585     _hideColumn: function(columnIdentifier)
586     {
587         var column = this.columns.get(columnIdentifier);
588         column.set("hidden", true);
589
590         var columnElement = column.get("element");
591         columnElement.style.width = 0;
592
593         this._columnWidthsInitialized = false;
594     },
595
596     get scrollContainer()
597     {
598         return this._scrollContainerElement;
599     },
600
601     isScrolledToLastRow: function()
602     {
603         return this._scrollContainerElement.isScrolledToBottom();
604     },
605
606     scrollToLastRow: function()
607     {
608         this._scrollContainerElement.scrollTop = this._scrollContainerElement.scrollHeight - this._scrollContainerElement.offsetHeight;
609     },
610
611     _positionResizerElements: function()
612     {
613         var left = 0;
614         var previousResizerElement = null;
615
616         // Make n - 1 resizers for n columns.
617         for (var i = 0; i < this.orderedColumns.length - 1; ++i) {
618             var resizerElement = this.resizerElements[i];
619
620             if (!resizerElement) {
621                 // This is the first call to updateWidth, so the resizers need
622                 // to be created.
623                 resizerElement = document.createElement("div");
624                 resizerElement.classList.add("data-grid-resizer");
625                 // This resizer is associated with the column to its right.
626                 resizerElement.addEventListener("mousedown", this._startResizerDragging.bind(this), false);
627                 this.element.appendChild(resizerElement);
628                 this.resizerElements[i] = resizerElement;
629             }
630
631             // Get the width of the cell in the first (and only) row of the
632             // header table in order to determine the width of the column, since
633             // it is not possible to query a column for its width.
634             left += this._headerTableBodyElement.rows[0].cells[i].offsetWidth;
635
636             if (this._isColumnVisible(this.orderedColumns[i])) {
637                 resizerElement.style.removeProperty("display");
638                 resizerElement.style.left = left + "px";
639                 resizerElement.leftNeighboringColumnID = i;
640                 if (previousResizerElement)
641                     previousResizerElement.rightNeighboringColumnID = i;
642                 previousResizerElement = resizerElement;
643             } else {
644                 resizerElement.style.setProperty("display", "none");
645                 resizerElement.leftNeighboringColumnID = 0;
646                 resizerElement.rightNeighboringColumnID = 0;
647             }
648         }
649         if (previousResizerElement)
650             previousResizerElement.rightNeighboringColumnID = this.orderedColumns.length - 1;
651     },
652
653     addPlaceholderNode: function()
654     {
655         if (this.placeholderNode)
656             this.placeholderNode.makeNormal();
657
658         var emptyData = {};
659         for (var identifier of this.columns.keys())
660             emptyData[identifier] = '';
661         this.placeholderNode = new WebInspector.PlaceholderDataGridNode(emptyData);
662         this.appendChild(this.placeholderNode);
663     },
664
665     appendChild: function(child)
666     {
667         this.insertChild(child, this.children.length);
668     },
669
670     insertChild: function(child, index)
671     {
672         if (!child)
673             throw("insertChild: Node can't be undefined or null.");
674         if (child.parent === this)
675             throw("insertChild: Node is already a child of this node.");
676
677         if (child.parent)
678             child.parent.removeChild(child);
679
680         this.children.splice(index, 0, child);
681         this.hasChildren = true;
682
683         child.parent = this;
684         child.dataGrid = this.dataGrid;
685         child._recalculateSiblings(index);
686
687         delete child._depth;
688         delete child._revealed;
689         delete child._attached;
690         child._shouldRefreshChildren = true;
691
692         var current = child.children[0];
693         while (current) {
694             current.dataGrid = this.dataGrid;
695             delete current._depth;
696             delete current._revealed;
697             delete current._attached;
698             current._shouldRefreshChildren = true;
699             current = current.traverseNextNode(false, child, true);
700         }
701
702         if (this.expanded)
703             child._attach();
704     },
705
706     removeChild: function(child)
707     {
708         if (!child)
709             throw("removeChild: Node can't be undefined or null.");
710         if (child.parent !== this)
711             throw("removeChild: Node is not a child of this node.");
712
713         child.deselect();
714         child._detach();
715
716         this.children.remove(child, true);
717
718         if (child.previousSibling)
719             child.previousSibling.nextSibling = child.nextSibling;
720         if (child.nextSibling)
721             child.nextSibling.previousSibling = child.previousSibling;
722
723         child.dataGrid = null;
724         child.parent = null;
725         child.nextSibling = null;
726         child.previousSibling = null;
727
728         if (this.children.length <= 0)
729             this.hasChildren = false;
730
731         console.assert(!child.isPlaceholderNode, "Shouldn't delete the placeholder node.")
732     },
733
734     removeChildren: function()
735     {
736         for (var i = 0; i < this.children.length; ++i) {
737             var child = this.children[i];
738             child.deselect();
739             child._detach();
740
741             child.dataGrid = null;
742             child.parent = null;
743             child.nextSibling = null;
744             child.previousSibling = null;
745         }
746
747         this.children = [];
748         this.hasChildren = false;
749     },
750
751     removeChildrenRecursive: function()
752     {
753         var childrenToRemove = this.children;
754
755         var child = this.children[0];
756         while (child) {
757             if (child.children.length)
758                 childrenToRemove = childrenToRemove.concat(child.children);
759             child = child.traverseNextNode(false, this, true);
760         }
761
762         for (var i = 0; i < childrenToRemove.length; ++i) {
763             child = childrenToRemove[i];
764             child.deselect();
765             child._detach();
766
767             child.children = [];
768             child.dataGrid = null;
769             child.parent = null;
770             child.nextSibling = null;
771             child.previousSibling = null;
772         }
773
774         this.children = [];
775     },
776
777     sortNodes: function(comparator)
778     {
779         function comparatorWrapper(aRow, bRow)
780         {
781             var reverseFactor = this.sortOrder !== "asceding" ? -1 : 1;
782             var aNode = aRow._dataGridNode;
783             var bNode = bRow._dataGridNode;
784             if (aNode._data.summaryRow || aNode.isPlaceholderNode)
785                 return 1;
786             if (bNode._data.summaryRow || bNode.isPlaceholderNode)
787                 return -1;
788
789             return reverseFactor * comparator(aNode, bNode);
790         }
791
792         if (this._editing) {
793             this._sortAfterEditingCallback = this.sortNodes.bind(this, comparator);
794             return;
795         }
796
797         var tbody = this.dataTableBodyElement;
798         var childNodes = tbody.childNodes;
799         var fillerRowElement = tbody.lastChild;
800
801         var sortedRowElements = Array.prototype.slice.call(childNodes, 0, childNodes.length - 1);
802         sortedRowElements.sort(comparatorWrapper);
803
804         tbody.removeChildren();
805
806         var previousSiblingNode = null;
807         for (var rowElement of sortedRowElements) {
808             var node = rowElement._dataGridNode;
809             node.previousSibling = previousSiblingNode;
810             if (previousSiblingNode)
811                 previousSiblingNode.nextSibling = node;
812             tbody.appendChild(rowElement);
813             previousSiblingNode = node;
814         }
815
816         if (previousSiblingNode)
817             previousSiblingNode.nextSibling = null;
818
819         tbody.appendChild(fillerRowElement); // We expect to find a filler row when attaching nodes.
820     },
821
822     _keyDown: function(event)
823     {
824         if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing)
825             return;
826
827         var handled = false;
828         var nextSelectedNode;
829         if (event.keyIdentifier === "Up" && !event.altKey) {
830             nextSelectedNode = this.selectedNode.traversePreviousNode(true);
831             while (nextSelectedNode && !nextSelectedNode.selectable)
832                 nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
833             handled = nextSelectedNode ? true : false;
834         } else if (event.keyIdentifier === "Down" && !event.altKey) {
835             nextSelectedNode = this.selectedNode.traverseNextNode(true);
836             while (nextSelectedNode && !nextSelectedNode.selectable)
837                 nextSelectedNode = nextSelectedNode.traverseNextNode(true);
838             handled = nextSelectedNode ? true : false;
839         } else if (event.keyIdentifier === "Left") {
840             if (this.selectedNode.expanded) {
841                 if (event.altKey)
842                     this.selectedNode.collapseRecursively();
843                 else
844                     this.selectedNode.collapse();
845                 handled = true;
846             } else if (this.selectedNode.parent && !this.selectedNode.parent.root) {
847                 handled = true;
848                 if (this.selectedNode.parent.selectable) {
849                     nextSelectedNode = this.selectedNode.parent;
850                     handled = nextSelectedNode ? true : false;
851                 } else if (this.selectedNode.parent)
852                     this.selectedNode.parent.collapse();
853             }
854         } else if (event.keyIdentifier === "Right") {
855             if (!this.selectedNode.revealed) {
856                 this.selectedNode.reveal();
857                 handled = true;
858             } else if (this.selectedNode.hasChildren) {
859                 handled = true;
860                 if (this.selectedNode.expanded) {
861                     nextSelectedNode = this.selectedNode.children[0];
862                     handled = nextSelectedNode ? true : false;
863                 } else {
864                     if (event.altKey)
865                         this.selectedNode.expandRecursively();
866                     else
867                         this.selectedNode.expand();
868                 }
869             }
870         } else if (event.keyCode === 8 || event.keyCode === 46) {
871             if (this._deleteCallback) {
872                 handled = true;
873                 this._deleteCallback(this.selectedNode);
874             }
875         } else if (isEnterKey(event)) {
876             if (this._editCallback) {
877                 handled = true;
878                 this._startEditing(this.selectedNode._element.children[0]);
879             }
880         }
881
882         if (nextSelectedNode) {
883             nextSelectedNode.reveal();
884             nextSelectedNode.select();
885         }
886
887         if (handled) {
888             event.preventDefault();
889             event.stopPropagation();
890         }
891     },
892
893     expand: function()
894     {
895         // This is the root, do nothing.
896     },
897
898     collapse: function()
899     {
900         // This is the root, do nothing.
901     },
902
903     reveal: function()
904     {
905         // This is the root, do nothing.
906     },
907
908     revealAndSelect: function()
909     {
910         // This is the root, do nothing.
911     },
912
913     dataGridNodeFromNode: function(target)
914     {
915         var rowElement = target.enclosingNodeOrSelfWithNodeName("tr");
916         return rowElement && rowElement._dataGridNode;
917     },
918
919     dataGridNodeFromPoint: function(x, y)
920     {
921         var node = this._dataTableElement.ownerDocument.elementFromPoint(x, y);
922         var rowElement = node.enclosingNodeOrSelfWithNodeName("tr");
923         return rowElement && rowElement._dataGridNode;
924     },
925
926     _clickInHeaderCell: function(event)
927     {
928         var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
929         if (!cell || !cell.columnIdentifier || !cell.classList.contains("sortable"))
930             return;
931
932         var sortOrder = this.sortOrder;
933
934         if (this._sortColumnCell)
935             this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
936
937         if (cell == this._sortColumnCell) {
938             if (sortOrder === "ascending")
939                 sortOrder = "descending";
940             else
941                 sortOrder = "ascending";
942         }
943
944         this._sortColumnCell = cell;
945
946         cell.classList.add("sort-" + sortOrder);
947
948         this.dispatchEventToListeners(WebInspector.DataGrid.Event.SortChanged);
949     },
950
951     _mouseoverColumnCollapser: function(event)
952     {
953         var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
954         if (!cell || !cell.collapsesGroup)
955             return;
956
957         cell.classList.add("mouse-over-collapser");
958     },
959
960     _mouseoutColumnCollapser: function(event)
961     {
962         var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
963         if (!cell || !cell.collapsesGroup)
964             return;
965
966         cell.classList.remove("mouse-over-collapser");
967     },
968
969     _clickInColumnCollapser: function(event)
970     {
971         var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
972         if (!cell || !cell.collapsesGroup)
973             return;
974
975         this._collapseColumnGroupWithCell(cell);
976
977         event.stopPropagation();
978         event.preventDefault();
979     },
980
981     collapseColumnGroup: function(columnGroup)
982     {
983         var collapserColumnIdentifier = null;
984         for (var [identifier, column] of this.columns) {
985             if (column.get("collapsesGroup") == columnGroup) {
986                 collapserColumnIdentifier = identifier;
987                 break;
988             }
989         }
990
991         console.assert(collapserColumnIdentifier);
992         if (!collapserColumnIdentifier)
993             return;
994
995         var cell = this._headerTableCellElements.get(collapserColumnIdentifier);
996         this._collapseColumnGroupWithCell(cell);
997     },
998
999     _collapseColumnGroupWithCell: function(cell)
1000     {
1001         var columnsWillCollapse = cell.classList.toggle("collapsed");
1002
1003         this.willToggleColumnGroup(cell.collapsesGroup, columnsWillCollapse);
1004
1005         var showOrHide = columnsWillCollapse ? this._hideColumn : this._showColumn;
1006         for (var [identifier, column] of this.columns) {
1007             if (column.get("group") === cell.collapsesGroup)
1008                 showOrHide.call(this, identifier);
1009         }
1010
1011         var collapserButton = cell.querySelector(".collapser-button");
1012         if (collapserButton)
1013             collapserButton.title = columnsWillCollapse ? this._collapserButtonExpandColumnsToolTip() : this._collapserButtonCollapseColumnsToolTip();
1014
1015         this.didToggleColumnGroup(cell.collapsesGroup, columnsWillCollapse);
1016     },
1017
1018     _collapserButtonCollapseColumnsToolTip: function()
1019     {
1020         return WebInspector.UIString("Collapse columns");
1021     },
1022
1023     _collapserButtonExpandColumnsToolTip: function()
1024     {
1025         return WebInspector.UIString("Expand columns");
1026     },
1027
1028     willToggleColumnGroup: function(columnGroup, willCollapse)
1029     {
1030         // Implemented by subclasses if needed.
1031     },
1032
1033     didToggleColumnGroup: function(columnGroup, didCollapse)
1034     {
1035         // Implemented by subclasses if needed.
1036     },
1037
1038     isColumnSortColumn: function(columnIdentifier)
1039     {
1040         return this._sortColumnCell === this._headerTableCellElements.get(columnIdentifier);
1041     },
1042
1043     markColumnAsSortedBy: function(columnIdentifier, sortOrder)
1044     {
1045         if (this._sortColumnCell)
1046             this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
1047         this._sortColumnCell = this._headerTableCellElements.get(columnIdentifier);
1048         this._sortColumnCell.classList.add("sort-" + sortOrder);
1049     },
1050
1051     headerTableHeader: function(columnIdentifier)
1052     {
1053         return this._headerTableCellElements.get(columnIdentifier);
1054     },
1055
1056     _generateSortIndicatorImagesIfNeeded: function()
1057     {
1058         if (WebInspector.DataGrid._generatedSortIndicatorImages)
1059             return;
1060
1061         WebInspector.DataGrid._generatedSortIndicatorImages = true;
1062
1063         var specifications = {};
1064         specifications["arrow"] = {
1065             fillColor: [81, 81, 81],
1066             shadowColor: [255, 255, 255, 0.5],
1067             shadowOffsetX: 0,
1068             shadowOffsetY: 1,
1069             shadowBlur: 0
1070         };
1071
1072         generateColoredImagesForCSS("Images/SortIndicatorDownArrow.svg", specifications, 9, 8, "data-grid-sort-indicator-down-");
1073         generateColoredImagesForCSS("Images/SortIndicatorUpArrow.svg", specifications, 9, 8, "data-grid-sort-indicator-up-");
1074     },
1075
1076     _mouseDownInDataTable: function(event)
1077     {
1078         var gridNode = this.dataGridNodeFromNode(event.target);
1079         if (!gridNode || !gridNode.selectable)
1080             return;
1081
1082         if (gridNode.isEventWithinDisclosureTriangle(event))
1083             return;
1084
1085         if (event.metaKey) {
1086             if (gridNode.selected)
1087                 gridNode.deselect();
1088             else
1089                 gridNode.select();
1090         } else
1091             gridNode.select();
1092     },
1093
1094     _contextMenuInDataTable: function(event)
1095     {
1096         var contextMenu = new WebInspector.ContextMenu(event);
1097
1098         var gridNode = this.dataGridNodeFromNode(event.target);
1099         if (this.dataGrid._refreshCallback && (!gridNode || gridNode !== this.placeholderNode))
1100             contextMenu.appendItem(WebInspector.UIString("Refresh"), this._refreshCallback.bind(this));
1101
1102         if (gridNode && gridNode.selectable && !gridNode.isEventWithinDisclosureTriangle(event)) {
1103             contextMenu.appendItem(WebInspector.UIString("Copy Row"), this._copyRow.bind(this, event.target));
1104
1105             if (this.dataGrid._editCallback) {
1106                 if (gridNode === this.placeholderNode)
1107                     contextMenu.appendItem(WebInspector.UIString("Add New"), this._startEditing.bind(this, event.target));
1108                 else {
1109                     var element = event.target.enclosingNodeOrSelfWithNodeName("td");
1110                     var columnIdentifier = element.__columnIdentifier;
1111                     var columnTitle = this.dataGrid.columns.get(columnIdentifier).get('title');
1112                     contextMenu.appendItem(WebInspector.UIString("Edit ā€œ%sā€").format(columnTitle), this._startEditing.bind(this, event.target));
1113                 }
1114             }
1115             if (this.dataGrid._deleteCallback && gridNode !== this.placeholderNode)
1116                 contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode));
1117         }
1118
1119         contextMenu.show();
1120     },
1121
1122     _clickInDataTable: function(event)
1123     {
1124         var gridNode = this.dataGridNodeFromNode(event.target);
1125         if (!gridNode || !gridNode.hasChildren)
1126             return;
1127
1128         if (!gridNode.isEventWithinDisclosureTriangle(event))
1129             return;
1130
1131         if (gridNode.expanded) {
1132             if (event.altKey)
1133                 gridNode.collapseRecursively();
1134             else
1135                 gridNode.collapse();
1136         } else {
1137             if (event.altKey)
1138                 gridNode.expandRecursively();
1139             else
1140                 gridNode.expand();
1141         }
1142     },
1143
1144     _copyTextForDataGridNode: function(node)
1145     {
1146         var fields = [];
1147         for (var identifier of node.dataGrid.orderedColumns)
1148             fields.push(node.data[identifier] || "");
1149
1150         var tabSeparatedValues = fields.join("\t");
1151         return tabSeparatedValues;
1152     },
1153
1154     handleBeforeCopyEvent: function(event)
1155     {
1156         if (this.selectedNode && window.getSelection().isCollapsed)
1157             event.preventDefault();
1158     },
1159
1160     handleCopyEvent: function(event)
1161     {
1162         if (!this.selectedNode || !window.getSelection().isCollapsed)
1163             return;
1164
1165         var copyText = this._copyTextForDataGridNode(this.selectedNode);
1166         event.clipboardData.setData("text/plain", copyText);
1167         event.stopPropagation();
1168         event.preventDefault();
1169     },
1170
1171     _copyRow: function(target)
1172     {
1173         var gridNode = this.dataGridNodeFromNode(target);
1174         if (!gridNode)
1175             return;
1176
1177         var copyText = this._copyTextForDataGridNode(gridNode);
1178         InspectorFrontendHost.copyText(copyText);
1179     },
1180
1181     get resizeMethod()
1182     {
1183         if (typeof this._resizeMethod === "undefined")
1184             return WebInspector.DataGrid.ResizeMethod.Nearest;
1185         return this._resizeMethod;
1186     },
1187
1188     set resizeMethod(method)
1189     {
1190         this._resizeMethod = method;
1191     },
1192
1193     _startResizerDragging: function(event)
1194     {
1195         if (event.button !== 0 || event.ctrlKey)
1196             return;
1197
1198         this._currentResizer = event.target;
1199         if (!this._currentResizer.rightNeighboringColumnID)
1200             return;
1201
1202         WebInspector.elementDragStart(this._currentResizer, this._resizerDragging.bind(this),
1203             this._endResizerDragging.bind(this), event, "col-resize");
1204     },
1205
1206     _resizerDragging: function(event)
1207     {
1208         if (event.button !== 0)
1209             return;
1210
1211         var resizer = this._currentResizer;
1212         if (!resizer)
1213             return;
1214
1215         // Constrain the dragpoint to be within the containing div of the
1216         // datagrid.
1217         var dragPoint = event.clientX - this.element.totalOffsetLeft;
1218         // Constrain the dragpoint to be within the space made up by the
1219         // column directly to the left and the column directly to the right.
1220         var leftCellIndex = resizer.leftNeighboringColumnID;
1221         var rightCellIndex = resizer.rightNeighboringColumnID;
1222         var firstRowCells = this._headerTableBodyElement.rows[0].cells;
1223         var leftEdgeOfPreviousColumn = 0;
1224         for (var i = 0; i < leftCellIndex; i++)
1225             leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth;
1226
1227         // Differences for other resize methods
1228         if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.Last) {
1229             rightCellIndex = this.resizerElements.length;
1230         } else if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.First) {
1231             leftEdgeOfPreviousColumn += firstRowCells[leftCellIndex].offsetWidth - firstRowCells[0].offsetWidth;
1232             leftCellIndex = 0;
1233         }
1234
1235         var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[leftCellIndex].offsetWidth + firstRowCells[rightCellIndex].offsetWidth;
1236
1237         // Give each column some padding so that they don't disappear.
1238         var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding;
1239         var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding;
1240
1241         dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum);
1242
1243         resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px";
1244
1245         var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / this._dataTableElement.offsetWidth) * 100) + "%";
1246         this._headerTableColumnGroupElement.children[leftCellIndex].style.width = percentLeftColumn;
1247         this._dataTableColumnGroupElement.children[leftCellIndex].style.width = percentLeftColumn;
1248
1249         var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / this._dataTableElement.offsetWidth) * 100) + "%";
1250         this._headerTableColumnGroupElement.children[rightCellIndex].style.width =  percentRightColumn;
1251         this._dataTableColumnGroupElement.children[rightCellIndex].style.width = percentRightColumn;
1252
1253         this._positionResizerElements();
1254         event.preventDefault();
1255         this.dispatchEventToListeners(WebInspector.DataGrid.Event.DidLayout);
1256     },
1257
1258     _endResizerDragging: function(event)
1259     {
1260         if (event.button !== 0)
1261             return;
1262
1263         WebInspector.elementDragEnd(event);
1264         this._currentResizer = null;
1265         this.dispatchEventToListeners(WebInspector.DataGrid.Event.DidLayout);
1266     },
1267
1268     ColumnResizePadding: 10,
1269
1270     CenterResizerOverBorderAdjustment: 3,
1271 }
1272
1273 WebInspector.DataGrid.ResizeMethod = {
1274     Nearest: "nearest",
1275     First: "first",
1276     Last: "last"
1277 };
1278
1279 WebInspector.DataGrid.prototype.__proto__ = WebInspector.Object.prototype;
1280
1281 /**
1282  * @constructor
1283  * @extends {WebInspector.Object}
1284  * @param {boolean=} hasChildren
1285  */
1286 WebInspector.DataGridNode = function(data, hasChildren)
1287 {
1288     this._expanded = false;
1289     this._selected = false;
1290     this._shouldRefreshChildren = true;
1291     this._data = data || {};
1292     this.hasChildren = hasChildren || false;
1293     this.children = [];
1294     this.dataGrid = null;
1295     this.parent = null;
1296     this.previousSibling = null;
1297     this.nextSibling = null;
1298     this.disclosureToggleWidth = 10;
1299 }
1300
1301 WebInspector.DataGridNode.prototype = {
1302     get selectable()
1303     {
1304         return !this._element || !this._element.classList.contains("hidden");
1305     },
1306
1307     get element()
1308     {
1309         if (this._element)
1310             return this._element;
1311
1312         if (!this.dataGrid)
1313             return null;
1314
1315         this._element = document.createElement("tr");
1316         this._element._dataGridNode = this;
1317
1318         if (this.hasChildren)
1319             this._element.classList.add("parent");
1320         if (this.expanded)
1321             this._element.classList.add("expanded");
1322         if (this.selected)
1323             this._element.classList.add("selected");
1324         if (this.revealed)
1325             this._element.classList.add("revealed");
1326
1327         this.createCells();
1328         return this._element;
1329     },
1330
1331     createCells: function()
1332     {
1333         for (var columnIdentifier of this.dataGrid.orderedColumns)
1334             this._element.appendChild(this.createCell(columnIdentifier));
1335     },
1336
1337     refreshIfNeeded: function()
1338     {
1339         if (!this._needsRefresh)
1340             return;
1341
1342         delete this._needsRefresh;
1343
1344         this.refresh();
1345     },
1346
1347     needsRefresh: function()
1348     {
1349         this._needsRefresh = true;
1350
1351         if (!this._revealed)
1352             return;
1353
1354         if (this._scheduledRefreshIdentifier)
1355             return;
1356
1357         this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
1358     },
1359
1360     get data()
1361     {
1362         return this._data;
1363     },
1364
1365     set data(x)
1366     {
1367         this._data = x || {};
1368         this.needsRefresh();
1369     },
1370
1371     get revealed()
1372     {
1373         if ("_revealed" in this)
1374             return this._revealed;
1375
1376         var currentAncestor = this.parent;
1377         while (currentAncestor && !currentAncestor.root) {
1378             if (!currentAncestor.expanded) {
1379                 this._revealed = false;
1380                 return false;
1381             }
1382
1383             currentAncestor = currentAncestor.parent;
1384         }
1385
1386         this._revealed = true;
1387         return true;
1388     },
1389
1390     set hasChildren(x)
1391     {
1392         if (this._hasChildren === x)
1393             return;
1394
1395         this._hasChildren = x;
1396
1397         if (!this._element)
1398             return;
1399
1400         if (this._hasChildren)
1401         {
1402             this._element.classList.add("parent");
1403             if (this.expanded)
1404                 this._element.classList.add("expanded");
1405         }
1406         else
1407         {
1408             this._element.classList.remove("parent");
1409             this._element.classList.remove("expanded");
1410         }
1411     },
1412
1413     get hasChildren()
1414     {
1415         return this._hasChildren;
1416     },
1417
1418     set revealed(x)
1419     {
1420         if (this._revealed === x)
1421             return;
1422
1423         this._revealed = x;
1424
1425         if (this._element) {
1426             if (this._revealed)
1427                 this._element.classList.add("revealed");
1428             else
1429                 this._element.classList.remove("revealed");
1430         }
1431
1432         this.refreshIfNeeded();
1433
1434         for (var i = 0; i < this.children.length; ++i)
1435             this.children[i].revealed = x && this.expanded;
1436     },
1437
1438     get depth()
1439     {
1440         if ("_depth" in this)
1441             return this._depth;
1442         if (this.parent && !this.parent.root)
1443             this._depth = this.parent.depth + 1;
1444         else
1445             this._depth = 0;
1446         return this._depth;
1447     },
1448
1449     get leftPadding()
1450     {
1451         if (typeof(this._leftPadding) === "number")
1452             return this._leftPadding;
1453         
1454         this._leftPadding = this.depth * this.dataGrid.indentWidth;
1455         return this._leftPadding;
1456     },
1457
1458     get shouldRefreshChildren()
1459     {
1460         return this._shouldRefreshChildren;
1461     },
1462
1463     set shouldRefreshChildren(x)
1464     {
1465         this._shouldRefreshChildren = x;
1466         if (x && this.expanded)
1467             this.expand();
1468     },
1469
1470     get selected()
1471     {
1472         return this._selected;
1473     },
1474
1475     set selected(x)
1476     {
1477         if (x)
1478             this.select();
1479         else
1480             this.deselect();
1481     },
1482
1483     get expanded()
1484     {
1485         return this._expanded;
1486     },
1487
1488     set expanded(x)
1489     {
1490         if (x)
1491             this.expand();
1492         else
1493             this.collapse();
1494     },
1495
1496     refresh: function()
1497     {
1498         if (!this._element || !this.dataGrid)
1499             return;
1500
1501         if (this._scheduledRefreshIdentifier) {
1502             cancelAnimationFrame(this._scheduledRefreshIdentifier);
1503             delete this._scheduledRefreshIdentifier;
1504         }
1505
1506         delete this._needsRefresh;
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 cellElement = document.createElement("td");
1520         cellElement.className = columnIdentifier + "-column";
1521         cellElement.__columnIdentifier = columnIdentifier;
1522
1523         var column = this.dataGrid.columns.get(columnIdentifier);
1524
1525         if (column.has("aligned"))
1526             cellElement.classList.add(column.get("aligned"));
1527
1528         if (column.has("group"))
1529             cellElement.classList.add("column-group-" + column.get("group"));
1530
1531         var div = cellElement.createChild("div");
1532         var content = this.createCellContent(columnIdentifier, cellElement);
1533         div.appendChild(content instanceof Node ? content : document.createTextNode(content));
1534
1535         if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
1536             cellElement.classList.add("disclosure");
1537             if (this.leftPadding)
1538                 cellElement.style.setProperty("padding-left", this.leftPadding + "px");
1539         }
1540
1541         return cellElement;
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 = this.dataGrid.orderedColumns.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.dataTableBodyElement.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.dataTableBodyElement.lastChild.classList.contains("filler"));
1804
1805         if (!nextElement)
1806             nextElement = this.dataGrid.dataTableBodyElement.lastChild;
1807
1808         this.dataGrid.dataTableBodyElement.insertBefore(this.element, nextElement);
1809
1810         if (this.expanded)
1811             for (var i = 0; i < this.children.length; ++i)
1812                 this.children[i]._attach();
1813     },
1814
1815     _detach: function()
1816     {
1817         if (!this._attached)
1818             return;
1819
1820         this._attached = false;
1821
1822         if (this._element && this._element.parentNode)
1823             this._element.parentNode.removeChild(this._element);
1824
1825         for (var i = 0; i < this.children.length; ++i)
1826             this.children[i]._detach();
1827     },
1828
1829     savePosition: function()
1830     {
1831         if (this._savedPosition)
1832             return;
1833
1834         if (!this.parent)
1835             throw("savePosition: Node must have a parent.");
1836         this._savedPosition = {
1837             parent: this.parent,
1838             index: this.parent.children.indexOf(this)
1839         };
1840     },
1841
1842     restorePosition: function()
1843     {
1844         if (!this._savedPosition)
1845             return;
1846
1847         if (this.parent !== this._savedPosition.parent)
1848             this._savedPosition.parent.insertChild(this, this._savedPosition.index);
1849
1850         delete this._savedPosition;
1851     }
1852 }
1853
1854 WebInspector.DataGridNode.prototype.__proto__ = WebInspector.Object.prototype;
1855
1856 // Used to create a new table row when entering new data by editing cells.
1857 WebInspector.PlaceholderDataGridNode = function(data)
1858 {
1859     WebInspector.DataGridNode.call(this, data, false);
1860     this.isPlaceholderNode = true;
1861 }
1862
1863 WebInspector.PlaceholderDataGridNode.prototype = {
1864     constructor: WebInspector.PlaceholderDataGridNode,
1865     __proto__: WebInspector.DataGridNode.prototype,
1866
1867     makeNormal: function()
1868     {
1869         delete this.isPlaceholderNode;
1870         delete this.makeNormal;
1871     }
1872 }