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