Web Inspector: Disable Copy Row in Timelines DataGrids, it does not currently provide...
[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         specifications["arrow"] = {
1095             fillColor: [81, 81, 81],
1096             shadowColor: [255, 255, 255, 0.5],
1097             shadowOffsetX: 0,
1098             shadowOffsetY: 1,
1099             shadowBlur: 0
1100         };
1101
1102         generateColoredImagesForCSS("Images/SortIndicatorDownArrow.svg", specifications, 9, 8, "data-grid-sort-indicator-down-");
1103         generateColoredImagesForCSS("Images/SortIndicatorUpArrow.svg", specifications, 9, 8, "data-grid-sort-indicator-up-");
1104     },
1105
1106     _mouseDownInDataTable: function(event)
1107     {
1108         var gridNode = this.dataGridNodeFromNode(event.target);
1109         if (!gridNode || !gridNode.selectable)
1110             return;
1111
1112         if (gridNode.isEventWithinDisclosureTriangle(event))
1113             return;
1114
1115         if (event.metaKey) {
1116             if (gridNode.selected)
1117                 gridNode.deselect();
1118             else
1119                 gridNode.select();
1120         } else
1121             gridNode.select();
1122     },
1123
1124     _contextMenuInDataTable: function(event)
1125     {
1126         var contextMenu = new WebInspector.ContextMenu(event);
1127
1128         var gridNode = this.dataGridNodeFromNode(event.target);
1129         if (this.dataGrid._refreshCallback && (!gridNode || gridNode !== this.placeholderNode))
1130             contextMenu.appendItem(WebInspector.UIString("Refresh"), this._refreshCallback.bind(this));
1131
1132         if (gridNode && gridNode.selectable && gridNode.copyable && !gridNode.isEventWithinDisclosureTriangle(event)) {
1133             contextMenu.appendItem(WebInspector.UIString("Copy Row"), this._copyRow.bind(this, event.target));
1134
1135             if (this.dataGrid._editCallback) {
1136                 if (gridNode === this.placeholderNode)
1137                     contextMenu.appendItem(WebInspector.UIString("Add New"), this._startEditing.bind(this, event.target));
1138                 else {
1139                     var element = event.target.enclosingNodeOrSelfWithNodeName("td");
1140                     var columnIdentifier = element.__columnIdentifier;
1141                     var columnTitle = this.dataGrid.columns.get(columnIdentifier).get("title");
1142                     contextMenu.appendItem(WebInspector.UIString("Edit ā€œ%sā€").format(columnTitle), this._startEditing.bind(this, event.target));
1143                 }
1144             }
1145             if (this.dataGrid._deleteCallback && gridNode !== this.placeholderNode)
1146                 contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode));
1147         }
1148
1149         contextMenu.show();
1150     },
1151
1152     _clickInDataTable: function(event)
1153     {
1154         var gridNode = this.dataGridNodeFromNode(event.target);
1155         if (!gridNode || !gridNode.hasChildren)
1156             return;
1157
1158         if (!gridNode.isEventWithinDisclosureTriangle(event))
1159             return;
1160
1161         if (gridNode.expanded) {
1162             if (event.altKey)
1163                 gridNode.collapseRecursively();
1164             else
1165                 gridNode.collapse();
1166         } else {
1167             if (event.altKey)
1168                 gridNode.expandRecursively();
1169             else
1170                 gridNode.expand();
1171         }
1172     },
1173
1174     _copyTextForDataGridNode: function(node)
1175     {
1176         var fields = [];
1177         for (var identifier of node.dataGrid.orderedColumns)
1178             fields.push(node.data[identifier] || "");
1179
1180         var tabSeparatedValues = fields.join("\t");
1181         return tabSeparatedValues;
1182     },
1183
1184     handleBeforeCopyEvent: function(event)
1185     {
1186         if (this.selectedNode && window.getSelection().isCollapsed)
1187             event.preventDefault();
1188     },
1189
1190     handleCopyEvent: function(event)
1191     {
1192         if (!this.selectedNode || !window.getSelection().isCollapsed)
1193             return;
1194
1195         var copyText = this._copyTextForDataGridNode(this.selectedNode);
1196         event.clipboardData.setData("text/plain", copyText);
1197         event.stopPropagation();
1198         event.preventDefault();
1199     },
1200
1201     _copyRow: function(target)
1202     {
1203         var gridNode = this.dataGridNodeFromNode(target);
1204         if (!gridNode)
1205             return;
1206
1207         var copyText = this._copyTextForDataGridNode(gridNode);
1208         InspectorFrontendHost.copyText(copyText);
1209     },
1210
1211     get resizeMethod()
1212     {
1213         if (typeof this._resizeMethod === "undefined")
1214             return WebInspector.DataGrid.ResizeMethod.Nearest;
1215         return this._resizeMethod;
1216     },
1217
1218     set resizeMethod(method)
1219     {
1220         this._resizeMethod = method;
1221     },
1222
1223     _startResizerDragging: function(event)
1224     {
1225         if (event.button !== 0 || event.ctrlKey)
1226             return;
1227
1228         this._currentResizer = event.target;
1229         if (!this._currentResizer.rightNeighboringColumnID)
1230             return;
1231
1232         WebInspector.elementDragStart(this._currentResizer, this._resizerDragging.bind(this),
1233             this._endResizerDragging.bind(this), event, "col-resize");
1234     },
1235
1236     _resizerDragging: function(event)
1237     {
1238         if (event.button !== 0)
1239             return;
1240
1241         var resizer = this._currentResizer;
1242         if (!resizer)
1243             return;
1244
1245         // Constrain the dragpoint to be within the containing div of the
1246         // datagrid.
1247         var dragPoint = event.clientX - this.element.totalOffsetLeft;
1248         // Constrain the dragpoint to be within the space made up by the
1249         // column directly to the left and the column directly to the right.
1250         var leftCellIndex = resizer.leftNeighboringColumnID;
1251         var rightCellIndex = resizer.rightNeighboringColumnID;
1252         var firstRowCells = this._headerTableBodyElement.rows[0].cells;
1253         var leftEdgeOfPreviousColumn = 0;
1254         for (var i = 0; i < leftCellIndex; i++)
1255             leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth;
1256
1257         // Differences for other resize methods
1258         if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.Last) {
1259             rightCellIndex = this.resizerElements.length;
1260         } else if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.First) {
1261             leftEdgeOfPreviousColumn += firstRowCells[leftCellIndex].offsetWidth - firstRowCells[0].offsetWidth;
1262             leftCellIndex = 0;
1263         }
1264
1265         var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[leftCellIndex].offsetWidth + firstRowCells[rightCellIndex].offsetWidth;
1266
1267         // Give each column some padding so that they don't disappear.
1268         var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding;
1269         var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding;
1270
1271         dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum);
1272
1273         resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px";
1274
1275         var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / this._dataTableElement.offsetWidth) * 100) + "%";
1276         this._headerTableColumnGroupElement.children[leftCellIndex].style.width = percentLeftColumn;
1277         this._dataTableColumnGroupElement.children[leftCellIndex].style.width = percentLeftColumn;
1278
1279         var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / this._dataTableElement.offsetWidth) * 100) + "%";
1280         this._headerTableColumnGroupElement.children[rightCellIndex].style.width =  percentRightColumn;
1281         this._dataTableColumnGroupElement.children[rightCellIndex].style.width = percentRightColumn;
1282
1283         this._positionResizerElements();
1284         event.preventDefault();
1285         this.dispatchEventToListeners(WebInspector.DataGrid.Event.DidLayout);
1286     },
1287
1288     _endResizerDragging: function(event)
1289     {
1290         if (event.button !== 0)
1291             return;
1292
1293         WebInspector.elementDragEnd(event);
1294         this._currentResizer = null;
1295         this.dispatchEventToListeners(WebInspector.DataGrid.Event.DidLayout);
1296     },
1297
1298     ColumnResizePadding: 10,
1299
1300     CenterResizerOverBorderAdjustment: 3,
1301 }
1302
1303 WebInspector.DataGrid.ResizeMethod = {
1304     Nearest: "nearest",
1305     First: "first",
1306     Last: "last"
1307 };
1308
1309 WebInspector.DataGrid.prototype.__proto__ = WebInspector.Object.prototype;
1310
1311 WebInspector.DataGridNode = function(data, hasChildren)
1312 {
1313     this._expanded = false;
1314     this._selected = false;
1315     this._copyable = true;
1316     this._shouldRefreshChildren = true;
1317     this._data = data || {};
1318     this.hasChildren = hasChildren || false;
1319     this.children = [];
1320     this.dataGrid = null;
1321     this.parent = null;
1322     this.previousSibling = null;
1323     this.nextSibling = null;
1324     this.disclosureToggleWidth = 10;
1325 }
1326
1327 WebInspector.DataGridNode.prototype = {
1328     get selectable()
1329     {
1330         return !this._element || !this._element.classList.contains("hidden");
1331     },
1332
1333     get copyable()
1334     {
1335         return this._copyable;
1336     },
1337
1338     set copyable(x)
1339     {
1340         this._copyable = x;
1341     },
1342
1343     get element()
1344     {
1345         if (this._element)
1346             return this._element;
1347
1348         if (!this.dataGrid)
1349             return null;
1350
1351         this._element = document.createElement("tr");
1352         this._element._dataGridNode = this;
1353
1354         if (this.hasChildren)
1355             this._element.classList.add("parent");
1356         if (this.expanded)
1357             this._element.classList.add("expanded");
1358         if (this.selected)
1359             this._element.classList.add("selected");
1360         if (this.revealed)
1361             this._element.classList.add("revealed");
1362
1363         this.createCells();
1364         return this._element;
1365     },
1366
1367     createCells: function()
1368     {
1369         for (var columnIdentifier of this.dataGrid.orderedColumns)
1370             this._element.appendChild(this.createCell(columnIdentifier));
1371     },
1372
1373     refreshIfNeeded: function()
1374     {
1375         if (!this._needsRefresh)
1376             return;
1377
1378         delete this._needsRefresh;
1379
1380         this.refresh();
1381     },
1382
1383     needsRefresh: function()
1384     {
1385         this._needsRefresh = true;
1386
1387         if (!this._revealed)
1388             return;
1389
1390         if (this._scheduledRefreshIdentifier)
1391             return;
1392
1393         this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
1394     },
1395
1396     get data()
1397     {
1398         return this._data;
1399     },
1400
1401     set data(x)
1402     {
1403         this._data = x || {};
1404         this.needsRefresh();
1405     },
1406
1407     get revealed()
1408     {
1409         if ("_revealed" in this)
1410             return this._revealed;
1411
1412         var currentAncestor = this.parent;
1413         while (currentAncestor && !currentAncestor.root) {
1414             if (!currentAncestor.expanded) {
1415                 this._revealed = false;
1416                 return false;
1417             }
1418
1419             currentAncestor = currentAncestor.parent;
1420         }
1421
1422         this._revealed = true;
1423         return true;
1424     },
1425
1426     set hasChildren(x)
1427     {
1428         if (this._hasChildren === x)
1429             return;
1430
1431         this._hasChildren = x;
1432
1433         if (!this._element)
1434             return;
1435
1436         if (this._hasChildren)
1437         {
1438             this._element.classList.add("parent");
1439             if (this.expanded)
1440                 this._element.classList.add("expanded");
1441         }
1442         else
1443         {
1444             this._element.classList.remove("parent");
1445             this._element.classList.remove("expanded");
1446         }
1447     },
1448
1449     get hasChildren()
1450     {
1451         return this._hasChildren;
1452     },
1453
1454     set revealed(x)
1455     {
1456         if (this._revealed === x)
1457             return;
1458
1459         this._revealed = x;
1460
1461         if (this._element) {
1462             if (this._revealed)
1463                 this._element.classList.add("revealed");
1464             else
1465                 this._element.classList.remove("revealed");
1466         }
1467
1468         this.refreshIfNeeded();
1469
1470         for (var i = 0; i < this.children.length; ++i)
1471             this.children[i].revealed = x && this.expanded;
1472     },
1473
1474     get depth()
1475     {
1476         if ("_depth" in this)
1477             return this._depth;
1478         if (this.parent && !this.parent.root)
1479             this._depth = this.parent.depth + 1;
1480         else
1481             this._depth = 0;
1482         return this._depth;
1483     },
1484
1485     get leftPadding()
1486     {
1487         if (typeof(this._leftPadding) === "number")
1488             return this._leftPadding;
1489
1490         this._leftPadding = this.depth * this.dataGrid.indentWidth;
1491         return this._leftPadding;
1492     },
1493
1494     get shouldRefreshChildren()
1495     {
1496         return this._shouldRefreshChildren;
1497     },
1498
1499     set shouldRefreshChildren(x)
1500     {
1501         this._shouldRefreshChildren = x;
1502         if (x && this.expanded)
1503             this.expand();
1504     },
1505
1506     get selected()
1507     {
1508         return this._selected;
1509     },
1510
1511     set selected(x)
1512     {
1513         if (x)
1514             this.select();
1515         else
1516             this.deselect();
1517     },
1518
1519     get expanded()
1520     {
1521         return this._expanded;
1522     },
1523
1524     set expanded(x)
1525     {
1526         if (x)
1527             this.expand();
1528         else
1529             this.collapse();
1530     },
1531
1532     refresh: function()
1533     {
1534         if (!this._element || !this.dataGrid)
1535             return;
1536
1537         if (this._scheduledRefreshIdentifier) {
1538             cancelAnimationFrame(this._scheduledRefreshIdentifier);
1539             delete this._scheduledRefreshIdentifier;
1540         }
1541
1542         delete this._needsRefresh;
1543
1544         this._element.removeChildren();
1545         this.createCells();
1546     },
1547
1548     updateLayout: function()
1549     {
1550         // Implemented by subclasses if needed.
1551     },
1552
1553     createCell: function(columnIdentifier)
1554     {
1555         var cellElement = document.createElement("td");
1556         cellElement.className = columnIdentifier + "-column";
1557         cellElement.__columnIdentifier = columnIdentifier;
1558
1559         var column = this.dataGrid.columns.get(columnIdentifier);
1560
1561         if (column["aligned"])
1562             cellElement.classList.add(column["aligned"]);
1563
1564         if (column["group"])
1565             cellElement.classList.add("column-group-" + column["group"]);
1566
1567         var div = cellElement.createChild("div");
1568         var content = this.createCellContent(columnIdentifier, cellElement);
1569         div.appendChild(content instanceof Node ? content : document.createTextNode(content));
1570
1571         if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
1572             cellElement.classList.add("disclosure");
1573             if (this.leftPadding)
1574                 cellElement.style.setProperty("padding-left", this.leftPadding + "px");
1575         }
1576
1577         return cellElement;
1578     },
1579
1580     createCellContent: function(columnIdentifier)
1581     {
1582         return this.data[columnIdentifier] || "\u200b"; // Zero width space to keep the cell from collapsing.
1583     },
1584
1585     elementWithColumnIdentifier: function(columnIdentifier)
1586     {
1587         var index = this.dataGrid.orderedColumns.indexOf(columnIdentifier);
1588         if (index === -1)
1589             return null;
1590
1591         return this._element.children[index];
1592     },
1593
1594     // Share these functions with DataGrid. They are written to work with a DataGridNode this object.
1595     appendChild: WebInspector.DataGrid.prototype.appendChild,
1596     insertChild: WebInspector.DataGrid.prototype.insertChild,
1597     removeChild: WebInspector.DataGrid.prototype.removeChild,
1598     removeChildren: WebInspector.DataGrid.prototype.removeChildren,
1599     removeChildrenRecursive: WebInspector.DataGrid.prototype.removeChildrenRecursive,
1600
1601     _recalculateSiblings: function(myIndex)
1602     {
1603         if (!this.parent)
1604             return;
1605
1606         var previousChild = (myIndex > 0 ? this.parent.children[myIndex - 1] : null);
1607
1608         if (previousChild) {
1609             previousChild.nextSibling = this;
1610             this.previousSibling = previousChild;
1611         } else
1612             this.previousSibling = null;
1613
1614         var nextChild = this.parent.children[myIndex + 1];
1615
1616         if (nextChild) {
1617             nextChild.previousSibling = this;
1618             this.nextSibling = nextChild;
1619         } else
1620             this.nextSibling = null;
1621     },
1622
1623     collapse: function()
1624     {
1625         if (this._element)
1626             this._element.classList.remove("expanded");
1627
1628         this._expanded = false;
1629
1630         for (var i = 0; i < this.children.length; ++i)
1631             this.children[i].revealed = false;
1632
1633         this.dispatchEventToListeners("collapsed");
1634
1635         if (this.dataGrid)
1636             this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.CollapsedNode, {dataGridNode: this});
1637     },
1638
1639     collapseRecursively: function()
1640     {
1641         var item = this;
1642         while (item) {
1643             if (item.expanded)
1644                 item.collapse();
1645             item = item.traverseNextNode(false, this, true);
1646         }
1647     },
1648
1649     expand: function()
1650     {
1651         if (!this.hasChildren || this.expanded)
1652             return;
1653
1654         if (this.revealed && !this._shouldRefreshChildren)
1655             for (var i = 0; i < this.children.length; ++i)
1656                 this.children[i].revealed = true;
1657
1658         if (this._shouldRefreshChildren) {
1659             for (var i = 0; i < this.children.length; ++i)
1660                 this.children[i]._detach();
1661
1662             this.dispatchEventToListeners("populate");
1663
1664             if (this._attached) {
1665                 for (var i = 0; i < this.children.length; ++i) {
1666                     var child = this.children[i];
1667                     if (this.revealed)
1668                         child.revealed = true;
1669                     child._attach();
1670                 }
1671             }
1672
1673             delete this._shouldRefreshChildren;
1674         }
1675
1676         if (this._element)
1677             this._element.classList.add("expanded");
1678
1679         this._expanded = true;
1680
1681         this.dispatchEventToListeners("expanded");
1682
1683         if (this.dataGrid)
1684             this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.ExpandedNode, {dataGridNode: this});
1685     },
1686
1687     expandRecursively: function()
1688     {
1689         var item = this;
1690         while (item) {
1691             item.expand();
1692             item = item.traverseNextNode(false, this);
1693         }
1694     },
1695
1696     reveal: function()
1697     {
1698         var currentAncestor = this.parent;
1699         while (currentAncestor && !currentAncestor.root) {
1700             if (!currentAncestor.expanded)
1701                 currentAncestor.expand();
1702             currentAncestor = currentAncestor.parent;
1703         }
1704
1705         this.element.scrollIntoViewIfNeeded(false);
1706
1707         this.dispatchEventToListeners("revealed");
1708     },
1709
1710     select: function(supressSelectedEvent)
1711     {
1712         if (!this.dataGrid || !this.selectable || this.selected)
1713             return;
1714
1715         if (this.dataGrid.selectedNode)
1716             this.dataGrid.selectedNode.deselect();
1717
1718         this._selected = true;
1719         this.dataGrid.selectedNode = this;
1720
1721         if (this._element)
1722             this._element.classList.add("selected");
1723
1724         if (!supressSelectedEvent)
1725             this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.SelectedNodeChanged);
1726     },
1727
1728     revealAndSelect: function()
1729     {
1730         this.reveal();
1731         this.select();
1732     },
1733
1734     deselect: function(supressDeselectedEvent)
1735     {
1736         if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
1737             return;
1738
1739         this._selected = false;
1740         this.dataGrid.selectedNode = null;
1741
1742         if (this._element)
1743             this._element.classList.remove("selected");
1744
1745         if (!supressDeselectedEvent)
1746             this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.SelectedNodeChanged);
1747     },
1748
1749     traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info)
1750     {
1751         if (!dontPopulate && this.hasChildren)
1752             this.dispatchEventToListeners("populate");
1753
1754         if (info)
1755             info.depthChange = 0;
1756
1757         var node = (!skipHidden || this.revealed) ? this.children[0] : null;
1758         if (node && (!skipHidden || this.expanded)) {
1759             if (info)
1760                 info.depthChange = 1;
1761             return node;
1762         }
1763
1764         if (this === stayWithin)
1765             return null;
1766
1767         node = (!skipHidden || this.revealed) ? this.nextSibling : null;
1768         if (node)
1769             return node;
1770
1771         node = this;
1772         while (node && !node.root && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) {
1773             if (info)
1774                 info.depthChange -= 1;
1775             node = node.parent;
1776         }
1777
1778         if (!node)
1779             return null;
1780
1781         return (!skipHidden || node.revealed) ? node.nextSibling : null;
1782     },
1783
1784     traversePreviousNode: function(skipHidden, dontPopulate)
1785     {
1786         var node = (!skipHidden || this.revealed) ? this.previousSibling : null;
1787         if (!dontPopulate && node && node.hasChildren)
1788             node.dispatchEventToListeners("populate");
1789
1790         while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children.lastValue : null)) {
1791             if (!dontPopulate && node.hasChildren)
1792                 node.dispatchEventToListeners("populate");
1793             node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children.lastValue : null);
1794         }
1795
1796         if (node)
1797             return node;
1798
1799         if (!this.parent || this.parent.root)
1800             return null;
1801
1802         return this.parent;
1803     },
1804
1805     isEventWithinDisclosureTriangle: function(event)
1806     {
1807         if (!this.hasChildren)
1808             return false;
1809         var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
1810         if (!cell.classList.contains("disclosure"))
1811             return false;
1812
1813         var left = cell.totalOffsetLeft + this.leftPadding;
1814         return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth;
1815     },
1816
1817     _attach: function()
1818     {
1819         if (!this.dataGrid || this._attached)
1820             return;
1821
1822         this._attached = true;
1823
1824         var nextElement = null;
1825
1826         var previousGridNode = this.traversePreviousNode(true, true);
1827         if (previousGridNode && previousGridNode.element.parentNode)
1828             nextElement = previousGridNode.element.nextSibling;
1829         else if (!previousGridNode)
1830             nextElement = this.dataGrid.dataTableBodyElement.firstChild;
1831
1832         // If there is no next grid node, then append before the last child since the last child is the filler row.
1833         console.assert(this.dataGrid.dataTableBodyElement.lastChild.classList.contains("filler"));
1834
1835         if (!nextElement)
1836             nextElement = this.dataGrid.dataTableBodyElement.lastChild;
1837
1838         this.dataGrid.dataTableBodyElement.insertBefore(this.element, nextElement);
1839
1840         if (this.expanded)
1841             for (var i = 0; i < this.children.length; ++i)
1842                 this.children[i]._attach();
1843     },
1844
1845     _detach: function()
1846     {
1847         if (!this._attached)
1848             return;
1849
1850         this._attached = false;
1851
1852         if (this._element && this._element.parentNode)
1853             this._element.parentNode.removeChild(this._element);
1854
1855         for (var i = 0; i < this.children.length; ++i)
1856             this.children[i]._detach();
1857     },
1858
1859     savePosition: function()
1860     {
1861         if (this._savedPosition)
1862             return;
1863
1864         if (!this.parent)
1865             throw("savePosition: Node must have a parent.");
1866         this._savedPosition = {
1867             parent: this.parent,
1868             index: this.parent.children.indexOf(this)
1869         };
1870     },
1871
1872     restorePosition: function()
1873     {
1874         if (!this._savedPosition)
1875             return;
1876
1877         if (this.parent !== this._savedPosition.parent)
1878             this._savedPosition.parent.insertChild(this, this._savedPosition.index);
1879
1880         delete this._savedPosition;
1881     }
1882 }
1883
1884 WebInspector.DataGridNode.prototype.__proto__ = WebInspector.Object.prototype;
1885
1886 // Used to create a new table row when entering new data by editing cells.
1887 WebInspector.PlaceholderDataGridNode = function(data)
1888 {
1889     WebInspector.DataGridNode.call(this, data, false);
1890     this.isPlaceholderNode = true;
1891 }
1892
1893 WebInspector.PlaceholderDataGridNode.prototype = {
1894     constructor: WebInspector.PlaceholderDataGridNode,
1895     __proto__: WebInspector.DataGridNode.prototype,
1896
1897     makeNormal: function()
1898     {
1899         delete this.isPlaceholderNode;
1900         delete this.makeNormal;
1901     }
1902 }