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