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