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