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