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