1/*
2 * Copyright (C) 2008 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/**
27 * @constructor
28 * @extends {WebInspector.View}
29 * @param {!Array.<!WebInspector.DataGrid.ColumnDescriptor>} columnsArray
30 * @param {function(WebInspector.DataGridNode, string, string, string)=} editCallback
31 * @param {function(WebInspector.DataGridNode)=} deleteCallback
32 * @param {function()=} refreshCallback
33 * @param {function(!WebInspector.ContextMenu, WebInspector.DataGridNode)=} contextMenuCallback
34 */
35WebInspector.DataGrid = function(columnsArray, editCallback, deleteCallback, refreshCallback, contextMenuCallback)
36{
37    WebInspector.View.call(this);
38    this.registerRequiredCSS("dataGrid.css");
39
40    this.element.className = "data-grid";
41    this.element.tabIndex = 0;
42    this.element.addEventListener("keydown", this._keyDown.bind(this), false);
43
44    this._headerTable = document.createElement("table");
45    this._headerTable.className = "header";
46    this._headerTableHeaders = {};
47
48    this._dataTable = document.createElement("table");
49    this._dataTable.className = "data";
50
51    this._dataTable.addEventListener("mousedown", this._mouseDownInDataTable.bind(this), true);
52    this._dataTable.addEventListener("click", this._clickInDataTable.bind(this), true);
53
54    this._dataTable.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true);
55
56    // FIXME: Add a createCallback which is different from editCallback and has different
57    // behavior when creating a new node.
58    if (editCallback)
59        this._dataTable.addEventListener("dblclick", this._ondblclick.bind(this), false);
60    this._editCallback = editCallback;
61    this._deleteCallback = deleteCallback;
62    this._refreshCallback = refreshCallback;
63    this._contextMenuCallback = contextMenuCallback;
64
65    this._scrollContainer = document.createElement("div");
66    this._scrollContainer.className = "data-container";
67    this._scrollContainer.appendChild(this._dataTable);
68
69    this.element.appendChild(this._headerTable);
70    this.element.appendChild(this._scrollContainer);
71
72    var headerRow = document.createElement("tr");
73    var columnGroup = document.createElement("colgroup");
74    columnGroup.span = columnsArray.length;
75
76    var fillerRow = document.createElement("tr");
77    fillerRow.className = "filler";
78
79    this._columnsArray = columnsArray;
80    this.columns = {};
81
82    for (var i = 0; i < columnsArray.length; ++i) {
83        var column = columnsArray[i];
84        column.ordinal = i;
85        var columnIdentifier = column.identifier = column.id || i;
86        this.columns[columnIdentifier] = column;
87        if (column.disclosure)
88            this.disclosureColumnIdentifier = columnIdentifier;
89
90        var col = document.createElement("col");
91        if (column.width)
92            col.style.width = column.width;
93        column.element = col;
94        columnGroup.appendChild(col);
95
96        var cell = document.createElement("th");
97        cell.className = columnIdentifier + "-column";
98        cell.columnIdentifier = columnIdentifier;
99        this._headerTableHeaders[columnIdentifier] = cell;
100
101        var div = document.createElement("div");
102        if (column.titleDOMFragment)
103            div.appendChild(column.titleDOMFragment);
104        else
105            div.textContent = column.title;
106        cell.appendChild(div);
107
108        if (column.sort) {
109            cell.addStyleClass("sort-" + column.sort);
110            this._sortColumnCell = cell;
111        }
112
113        if (column.sortable) {
114            cell.addEventListener("click", this._clickInHeaderCell.bind(this), false);
115            cell.addStyleClass("sortable");
116        }
117
118        headerRow.appendChild(cell);
119        fillerRow.createChild("td", columnIdentifier + "-column");
120    }
121
122    headerRow.createChild("th", "corner");
123    fillerRow.createChild("td", "corner");
124
125    this._headerTableColumnGroup = columnGroup;
126    this._headerTable.appendChild(this._headerTableColumnGroup);
127    this.headerTableBody.appendChild(headerRow);
128
129    this._dataTableColumnGroup = columnGroup.cloneNode(true);
130    this._dataTable.appendChild(this._dataTableColumnGroup);
131    this.dataTableBody.appendChild(fillerRow);
132
133    this.selectedNode = null;
134    this.expandNodesWhenArrowing = false;
135    this.setRootNode(new WebInspector.DataGridNode());
136    this.indentWidth = 15;
137    this.resizers = [];
138    this._columnWidthsInitialized = false;
139}
140
141/** @typedef {{id: ?string, editable: boolean, sort: WebInspector.DataGrid.Order, sortable: boolean, align: WebInspector.DataGrid.Align}} */
142WebInspector.DataGrid.ColumnDescriptor;
143
144WebInspector.DataGrid.Events = {
145    SelectedNode: "SelectedNode",
146    DeselectedNode: "DeselectedNode",
147    SortingChanged: "SortingChanged",
148    ColumnsResized: "ColumnsResized"
149}
150
151/** @enum {string} */
152WebInspector.DataGrid.Order = {
153    Ascending: "ascending",
154    Descending: "descending"
155}
156
157/** @enum {string} */
158WebInspector.DataGrid.Align = {
159    Center: "center",
160    Right: "right"
161}
162
163/**
164 * @param {Array.<string>} columnNames
165 * @param {Array.<string>} values
166 */
167WebInspector.DataGrid.createSortableDataGrid = function(columnNames, values)
168{
169    var numColumns = columnNames.length;
170    if (!numColumns)
171        return null;
172
173    var columns = [];
174    for (var i = 0; i < columnNames.length; ++i)
175        columns.push({title: columnNames[i], width: columnNames[i].length, sortable: true});
176
177    var nodes = [];
178    for (var i = 0; i < values.length / numColumns; ++i) {
179        var data = {};
180        for (var j = 0; j < columnNames.length; ++j)
181            data[j] = values[numColumns * i + j];
182
183        var node = new WebInspector.DataGridNode(data, false);
184        node.selectable = false;
185        nodes.push(node);
186    }
187
188    var dataGrid = new WebInspector.DataGrid(columns);
189    var length = nodes.length;
190    for (var i = 0; i < length; ++i)
191        dataGrid.rootNode().appendChild(nodes[i]);
192
193    dataGrid.addEventListener(WebInspector.DataGrid.Events.SortingChanged, sortDataGrid, this);
194
195    function sortDataGrid()
196    {
197        var nodes = dataGrid._rootNode.children.slice();
198        var sortColumnIdentifier = dataGrid.sortColumnIdentifier();
199        var sortDirection = dataGrid.isSortOrderAscending() ? 1 : -1;
200        var columnIsNumeric = true;
201
202        for (var i = 0; i < nodes.length; i++) {
203            if (isNaN(Number(nodes[i].data[sortColumnIdentifier])))
204                columnIsNumeric = false;
205        }
206
207        function comparator(dataGridNode1, dataGridNode2)
208        {
209            var item1 = dataGridNode1.data[sortColumnIdentifier];
210            var item2 = dataGridNode2.data[sortColumnIdentifier];
211            item1 = item1 instanceof Node ? item1.textContent : String(item1);
212            item2 = item2 instanceof Node ? item2.textContent : String(item2);
213
214            var comparison;
215            if (columnIsNumeric) {
216                // Sort numbers based on comparing their values rather than a lexicographical comparison.
217                var number1 = parseFloat(item1);
218                var number2 = parseFloat(item2);
219                comparison = number1 < number2 ? -1 : (number1 > number2 ? 1 : 0);
220            } else
221                comparison = item1 < item2 ? -1 : (item1 > item2 ? 1 : 0);
222
223            return sortDirection * comparison;
224        }
225
226        nodes.sort(comparator);
227        dataGrid.rootNode().removeChildren();
228        for (var i = 0; i < nodes.length; i++)
229            dataGrid._rootNode.appendChild(nodes[i]);
230    }
231    return dataGrid;
232}
233
234WebInspector.DataGrid.prototype = {
235    /**
236     * @param {!WebInspector.DataGridNode} rootNode
237     */
238    setRootNode: function(rootNode)
239    {
240        if (this._rootNode) {
241            this._rootNode.removeChildren();
242            this._rootNode.dataGrid = null;
243            this._rootNode._isRoot = false;
244        }
245        /** @type {!WebInspector.DataGridNode} */
246        this._rootNode = rootNode;
247        rootNode._isRoot = true;
248        rootNode.hasChildren = false;
249        rootNode._expanded = true;
250        rootNode._revealed = true;
251        rootNode.dataGrid = this;
252    },
253
254    /**
255     * @return {!WebInspector.DataGridNode}
256     */
257    rootNode: function()
258    {
259        return this._rootNode;
260    },
261
262    _ondblclick: function(event)
263    {
264        if (this._editing || this._editingNode)
265            return;
266
267        var columnIdentifier = this.columnIdentifierFromNode(event.target);
268        if (!columnIdentifier || !this.columns[columnIdentifier].editable)
269            return;
270        this._startEditing(event.target);
271    },
272
273    /**
274     * @param {!WebInspector.DataGridNode} node
275     * @param {number} columnOrdinal
276     */
277    _startEditingColumnOfDataGridNode: function(node, columnOrdinal)
278    {
279        this._editing = true;
280        /** @type {WebInspector.DataGridNode} */
281        this._editingNode = node;
282        this._editingNode.select();
283
284        var element = this._editingNode._element.children[columnOrdinal];
285        WebInspector.startEditing(element, this._startEditingConfig(element));
286        window.getSelection().setBaseAndExtent(element, 0, element, 1);
287    },
288
289    _startEditing: function(target)
290    {
291        var element = target.enclosingNodeOrSelfWithNodeName("td");
292        if (!element)
293            return;
294
295        this._editingNode = this.dataGridNodeFromNode(target);
296        if (!this._editingNode) {
297            if (!this.creationNode)
298                return;
299            this._editingNode = this.creationNode;
300        }
301
302        // Force editing the 1st column when editing the creation node
303        if (this._editingNode.isCreationNode)
304            return this._startEditingColumnOfDataGridNode(this._editingNode, this._nextEditableColumn(-1));
305
306        this._editing = true;
307        WebInspector.startEditing(element, this._startEditingConfig(element));
308
309        window.getSelection().setBaseAndExtent(element, 0, element, 1);
310    },
311
312    renderInline: function()
313    {
314        this.element.addStyleClass("inline");
315    },
316
317    _startEditingConfig: function(element)
318    {
319        return new WebInspector.EditingConfig(this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent);
320    },
321
322    _editingCommitted: function(element, newText, oldText, context, moveDirection)
323    {
324        var columnIdentifier = this.columnIdentifierFromNode(element);
325        if (!columnIdentifier) {
326            this._editingCancelled(element);
327            return;
328        }
329        var columnOrdinal = this.columns[columnIdentifier].ordinal;
330        var textBeforeEditing = this._editingNode.data[columnIdentifier];
331        var currentEditingNode = this._editingNode;
332
333        function moveToNextIfNeeded(wasChange) {
334            if (!moveDirection)
335                return;
336
337            if (moveDirection === "forward") {
338            var firstEditableColumn = this._nextEditableColumn(-1);
339                if (currentEditingNode.isCreationNode && columnOrdinal === firstEditableColumn && !wasChange)
340                    return;
341
342                var nextEditableColumn = this._nextEditableColumn(columnOrdinal);
343                if (nextEditableColumn !== -1)
344                    return this._startEditingColumnOfDataGridNode(currentEditingNode, nextEditableColumn);
345
346                var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true);
347                if (nextDataGridNode)
348                    return this._startEditingColumnOfDataGridNode(nextDataGridNode, firstEditableColumn);
349                if (currentEditingNode.isCreationNode && wasChange) {
350                    this.addCreationNode(false);
351                    return this._startEditingColumnOfDataGridNode(this.creationNode, firstEditableColumn);
352                }
353                return;
354            }
355
356            if (moveDirection === "backward") {
357                var prevEditableColumn = this._nextEditableColumn(columnOrdinal, true);
358                if (prevEditableColumn !== -1)
359                    return this._startEditingColumnOfDataGridNode(currentEditingNode, prevEditableColumn);
360
361                var lastEditableColumn = this._nextEditableColumn(this._columnsArray.length, true);
362                var nextDataGridNode = currentEditingNode.traversePreviousNode(true, true);
363                if (nextDataGridNode)
364                    return this._startEditingColumnOfDataGridNode(nextDataGridNode, lastEditableColumn);
365                return;
366            }
367        }
368
369        if (textBeforeEditing == newText) {
370            this._editingCancelled(element);
371            moveToNextIfNeeded.call(this, false);
372            return;
373        }
374
375        // Update the text in the datagrid that we typed
376        this._editingNode.data[columnIdentifier] = newText;
377
378        // Make the callback - expects an editing node (table row), the column number that is being edited,
379        // the text that used to be there, and the new text.
380        this._editCallback(this._editingNode, columnIdentifier, textBeforeEditing, newText);
381
382        if (this._editingNode.isCreationNode)
383            this.addCreationNode(false);
384
385        this._editingCancelled(element);
386        moveToNextIfNeeded.call(this, true);
387    },
388
389    _editingCancelled: function(element)
390    {
391        delete this._editing;
392        this._editingNode = null;
393    },
394
395    /**
396     * @param {number} columnOrdinal
397     * @param {boolean=} moveBackward
398     * @return {number}
399     */
400    _nextEditableColumn: function(columnOrdinal, moveBackward)
401    {
402        var increment = moveBackward ? -1 : 1;
403        var columns = this._columnsArray;
404        for (var i = columnOrdinal + increment; (i >= 0) && (i < columns.length); i += increment) {
405            if (columns[i].editable)
406                return i;
407        }
408        return -1;
409    },
410
411    /**
412     * @return {?string}
413     */
414    sortColumnIdentifier: function()
415    {
416        if (!this._sortColumnCell)
417            return null;
418        return this._sortColumnCell.columnIdentifier;
419    },
420
421    /**
422     * @return {?string}
423     */
424    sortOrder: function()
425    {
426        if (!this._sortColumnCell || this._sortColumnCell.hasStyleClass("sort-ascending"))
427            return WebInspector.DataGrid.Order.Ascending;
428        if (this._sortColumnCell.hasStyleClass("sort-descending"))
429            return WebInspector.DataGrid.Order.Descending;
430        return null;
431    },
432
433    /**
434     * @return {boolean}
435     */
436    isSortOrderAscending: function()
437    {
438        return !this._sortColumnCell || this._sortColumnCell.hasStyleClass("sort-ascending");
439    },
440
441    get headerTableBody()
442    {
443        if ("_headerTableBody" in this)
444            return this._headerTableBody;
445
446        this._headerTableBody = this._headerTable.getElementsByTagName("tbody")[0];
447        if (!this._headerTableBody) {
448            this._headerTableBody = this.element.ownerDocument.createElement("tbody");
449            this._headerTable.insertBefore(this._headerTableBody, this._headerTable.tFoot);
450        }
451
452        return this._headerTableBody;
453    },
454
455    get dataTableBody()
456    {
457        if ("_dataTableBody" in this)
458            return this._dataTableBody;
459
460        this._dataTableBody = this._dataTable.getElementsByTagName("tbody")[0];
461        if (!this._dataTableBody) {
462            this._dataTableBody = this.element.ownerDocument.createElement("tbody");
463            this._dataTable.insertBefore(this._dataTableBody, this._dataTable.tFoot);
464        }
465
466        return this._dataTableBody;
467    },
468
469    /**
470     * @param {Array.<number>} widths
471     * @param {number} minPercent
472     * @param {number=} maxPercent
473     */
474    _autoSizeWidths: function(widths, minPercent, maxPercent)
475    {
476        if (minPercent)
477            minPercent = Math.min(minPercent, Math.floor(100 / widths.length));
478        var totalWidth = 0;
479        for (var i = 0; i < widths.length; ++i)
480            totalWidth += widths[i];
481        var totalPercentWidth = 0;
482        for (var i = 0; i < widths.length; ++i) {
483            var width = Math.round(100 * widths[i] / totalWidth);
484            if (minPercent && width < minPercent)
485                width = minPercent;
486            else if (maxPercent && width > maxPercent)
487                width = maxPercent;
488            totalPercentWidth += width;
489            widths[i] = width;
490        }
491        var recoupPercent = totalPercentWidth - 100;
492
493        while (minPercent && recoupPercent > 0) {
494            for (var i = 0; i < widths.length; ++i) {
495                if (widths[i] > minPercent) {
496                    --widths[i];
497                    --recoupPercent;
498                    if (!recoupPercent)
499                        break;
500                }
501            }
502        }
503
504        while (maxPercent && recoupPercent < 0) {
505            for (var i = 0; i < widths.length; ++i) {
506                if (widths[i] < maxPercent) {
507                    ++widths[i];
508                    ++recoupPercent;
509                    if (!recoupPercent)
510                        break;
511                }
512            }
513        }
514
515        return widths;
516    },
517
518    /**
519     * @param {number} minPercent
520     * @param {number=} maxPercent
521     * @param {number=} maxDescentLevel
522     */
523    autoSizeColumns: function(minPercent, maxPercent, maxDescentLevel)
524    {
525        var widths = [];
526        for (var i = 0; i < this._columnsArray.length; ++i)
527            widths.push((this._columnsArray[i].title || "").length);
528
529        maxDescentLevel = maxDescentLevel || 0;
530        var children = this._enumerateChildren(this._rootNode, [], maxDescentLevel + 1);
531        for (var i = 0; i < children.length; ++i) {
532            var node = children[i];
533            for (var j = 0; j < this._columnsArray.length; ++j) {
534                var text = node.data[this._columnsArray[j].identifier] || "";
535                if (text.length > widths[j])
536                    widths[j] = text.length;
537            }
538        }
539
540        widths = this._autoSizeWidths(widths, minPercent, maxPercent);
541
542        for (var i = 0; i < this._columnsArray.length; ++i)
543            this._columnsArray[i].element.style.width = widths[i] + "%";
544        this._columnWidthsInitialized = false;
545        this.updateWidths();
546    },
547
548    _enumerateChildren: function(rootNode, result, maxLevel)
549    {
550        if (!rootNode._isRoot)
551            result.push(rootNode);
552        if (!maxLevel)
553            return;
554        for (var i = 0; i < rootNode.children.length; ++i)
555            this._enumerateChildren(rootNode.children[i], result, maxLevel - 1);
556        return result;
557    },
558
559    onResize: function()
560    {
561        this.updateWidths();
562    },
563
564    // Updates the widths of the table, including the positions of the column
565    // resizers.
566    //
567    // IMPORTANT: This function MUST be called once after the element of the
568    // DataGrid is attached to its parent element and every subsequent time the
569    // width of the parent element is changed in order to make it possible to
570    // resize the columns.
571    //
572    // If this function is not called after the DataGrid is attached to its
573    // parent element, then the DataGrid's columns will not be resizable.
574    updateWidths: function()
575    {
576        var headerTableColumns = this._headerTableColumnGroup.children;
577
578        var tableWidth = this._dataTable.offsetWidth;
579        var numColumns = headerTableColumns.length;
580
581        // Do not attempt to use offsetes if we're not attached to the document tree yet.
582        if (!this._columnWidthsInitialized && this.element.offsetWidth) {
583            // Give all the columns initial widths now so that during a resize,
584            // when the two columns that get resized get a percent value for
585            // their widths, all the other columns already have percent values
586            // for their widths.
587            for (var i = 0; i < numColumns; i++) {
588                var columnWidth = this.headerTableBody.rows[0].cells[i].offsetWidth;
589                var percentWidth = ((columnWidth / tableWidth) * 100) + "%";
590                this._headerTableColumnGroup.children[i].style.width = percentWidth;
591                this._dataTableColumnGroup.children[i].style.width = percentWidth;
592            }
593            this._columnWidthsInitialized = true;
594        }
595        this._positionResizers();
596        this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
597    },
598
599    applyColumnWeights: function()
600    {
601        var sumOfWeights = 0.0;
602        for (var i = 0; i < this._columnsArray.length; ++i) {
603            var column = this._columnsArray[i];
604            if (this.isColumnVisible(column))
605                sumOfWeights += column.weight;
606        }
607        var factor = 100 / sumOfWeights;
608
609        for (var i = 0; i < this._columnsArray.length; ++i) {
610            var column = this._columnsArray[i];
611            var width = this.isColumnVisible(column) ? ((factor * column.weight) + "%"): "0%";
612            this._headerTableColumnGroup.children[i].style.width = width;
613            this._dataTableColumnGroup.children[i].style.width = width;
614        }
615
616        this._positionResizers();
617        this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
618    },
619
620    /**
621     * @param {!WebInspector.DataGrid.ColumnDescriptor} column
622     * @return {boolean}
623     */
624    isColumnVisible: function(column)
625    {
626        return !column.hidden;
627    },
628
629    /**
630     * @param {string} columnIdentifier
631     * @param {boolean} visible
632     */
633    setColumnVisible: function(columnIdentifier, visible)
634    {
635        if (visible === !this.columns[columnIdentifier].hidden)
636            return;
637
638        this.columns[columnIdentifier].hidden = !visible;
639        this.element.enableStyleClass("hide-" + columnIdentifier + "-column", !visible);
640    },
641
642    get scrollContainer()
643    {
644        return this._scrollContainer;
645    },
646
647    isScrolledToLastRow: function()
648    {
649        return this._scrollContainer.isScrolledToBottom();
650    },
651
652    scrollToLastRow: function()
653    {
654        this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight - this._scrollContainer.offsetHeight;
655    },
656
657    _positionResizers: function()
658    {
659        var headerTableColumns = this._headerTableColumnGroup.children;
660        var numColumns = headerTableColumns.length;
661        var left = 0;
662        var previousResizer = null;
663
664        // Make n - 1 resizers for n columns.
665        for (var i = 0; i < numColumns - 1; i++) {
666            var resizer = this.resizers[i];
667
668            if (!resizer) {
669                // This is the first call to updateWidth, so the resizers need
670                // to be created.
671                resizer = document.createElement("div");
672                resizer.addStyleClass("data-grid-resizer");
673                // This resizer is associated with the column to its right.
674                WebInspector.installDragHandle(resizer, this._startResizerDragging.bind(this), this._resizerDragging.bind(this), this._endResizerDragging.bind(this), "col-resize");
675                this.element.appendChild(resizer);
676                this.resizers[i] = resizer;
677            }
678
679            // Get the width of the cell in the first (and only) row of the
680            // header table in order to determine the width of the column, since
681            // it is not possible to query a column for its width.
682            left += this.headerTableBody.rows[0].cells[i].offsetWidth;
683
684            if (!this._columnsArray[i].hidden) {
685                resizer.style.removeProperty("display");
686                if (resizer._position !== left) {
687                    resizer._position = left;
688                    resizer.style.left = left + "px";
689                }
690                resizer.leftNeighboringColumnIndex = i;
691                if (previousResizer)
692                    previousResizer.rightNeighboringColumnIndex = i;
693                previousResizer = resizer;
694            } else {
695                if (previousResizer && previousResizer._position !== left) {
696                    previousResizer._position = left;
697                    previousResizer.style.left = left + "px";
698                }
699                resizer.style.setProperty("display", "none");
700                resizer.leftNeighboringColumnIndex = 0;
701                resizer.rightNeighboringColumnIndex = 0;
702            }
703        }
704        if (previousResizer)
705            previousResizer.rightNeighboringColumnIndex = numColumns - 1;
706    },
707
708    addCreationNode: function(hasChildren)
709    {
710        if (this.creationNode)
711            this.creationNode.makeNormal();
712
713        var emptyData = {};
714        for (var column in this.columns)
715            emptyData[column] = '';
716        this.creationNode = new WebInspector.CreationDataGridNode(emptyData, hasChildren);
717        this.rootNode().appendChild(this.creationNode);
718    },
719
720    sortNodes: function(comparator, reverseMode)
721    {
722        function comparatorWrapper(a, b)
723        {
724            if (a._dataGridNode._data.summaryRow)
725                return 1;
726            if (b._dataGridNode._data.summaryRow)
727                return -1;
728
729            var aDataGirdNode = a._dataGridNode;
730            var bDataGirdNode = b._dataGridNode;
731            return reverseMode ? comparator(bDataGirdNode, aDataGirdNode) : comparator(aDataGirdNode, bDataGirdNode);
732        }
733
734        var tbody = this.dataTableBody;
735        var tbodyParent = tbody.parentElement;
736        tbodyParent.removeChild(tbody);
737
738        var childNodes = tbody.childNodes;
739        var fillerRow = childNodes[childNodes.length - 1];
740
741        var sortedRows = Array.prototype.slice.call(childNodes, 0, childNodes.length - 1);
742        sortedRows.sort(comparatorWrapper);
743        var sortedRowsLength = sortedRows.length;
744
745        tbody.removeChildren();
746        var previousSiblingNode = null;
747        for (var i = 0; i < sortedRowsLength; ++i) {
748            var row = sortedRows[i];
749            var node = row._dataGridNode;
750            node.previousSibling = previousSiblingNode;
751            if (previousSiblingNode)
752                previousSiblingNode.nextSibling = node;
753            tbody.appendChild(row);
754            previousSiblingNode = node;
755        }
756        if (previousSiblingNode)
757            previousSiblingNode.nextSibling = null;
758
759        tbody.appendChild(fillerRow);
760        tbodyParent.appendChild(tbody);
761    },
762
763    _keyDown: function(event)
764    {
765        if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing)
766            return;
767
768        var handled = false;
769        var nextSelectedNode;
770        if (event.keyIdentifier === "Up" && !event.altKey) {
771            nextSelectedNode = this.selectedNode.traversePreviousNode(true);
772            while (nextSelectedNode && !nextSelectedNode.selectable)
773                nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
774            handled = nextSelectedNode ? true : false;
775        } else if (event.keyIdentifier === "Down" && !event.altKey) {
776            nextSelectedNode = this.selectedNode.traverseNextNode(true);
777            while (nextSelectedNode && !nextSelectedNode.selectable)
778                nextSelectedNode = nextSelectedNode.traverseNextNode(true);
779            handled = nextSelectedNode ? true : false;
780        } else if (event.keyIdentifier === "Left") {
781            if (this.selectedNode.expanded) {
782                if (event.altKey)
783                    this.selectedNode.collapseRecursively();
784                else
785                    this.selectedNode.collapse();
786                handled = true;
787            } else if (this.selectedNode.parent && !this.selectedNode.parent._isRoot) {
788                handled = true;
789                if (this.selectedNode.parent.selectable) {
790                    nextSelectedNode = this.selectedNode.parent;
791                    handled = nextSelectedNode ? true : false;
792                } else if (this.selectedNode.parent)
793                    this.selectedNode.parent.collapse();
794            }
795        } else if (event.keyIdentifier === "Right") {
796            if (!this.selectedNode.revealed) {
797                this.selectedNode.reveal();
798                handled = true;
799            } else if (this.selectedNode.hasChildren) {
800                handled = true;
801                if (this.selectedNode.expanded) {
802                    nextSelectedNode = this.selectedNode.children[0];
803                    handled = nextSelectedNode ? true : false;
804                } else {
805                    if (event.altKey)
806                        this.selectedNode.expandRecursively();
807                    else
808                        this.selectedNode.expand();
809                }
810            }
811        } else if (event.keyCode === 8 || event.keyCode === 46) {
812            if (this._deleteCallback) {
813                handled = true;
814                this._deleteCallback(this.selectedNode);
815                this.changeNodeAfterDeletion();
816            }
817        } else if (isEnterKey(event)) {
818            if (this._editCallback) {
819                handled = true;
820                this._startEditing(this.selectedNode._element.children[this._nextEditableColumn(-1)]);
821            }
822        }
823
824        if (nextSelectedNode) {
825            nextSelectedNode.reveal();
826            nextSelectedNode.select();
827        }
828
829        if (handled)
830            event.consume(true);
831    },
832
833    changeNodeAfterDeletion: function()
834    {
835        var nextSelectedNode = this.selectedNode.traverseNextNode(true);
836        while (nextSelectedNode && !nextSelectedNode.selectable)
837            nextSelectedNode = nextSelectedNode.traverseNextNode(true);
838
839        if (!nextSelectedNode || nextSelectedNode.isCreationNode) {
840            nextSelectedNode = this.selectedNode.traversePreviousNode(true);
841            while (nextSelectedNode && !nextSelectedNode.selectable)
842                nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
843        }
844
845        if (nextSelectedNode) {
846            nextSelectedNode.reveal();
847            nextSelectedNode.select();
848        }
849    },
850
851    /**
852     * @param {!Node} target
853     * @return {?WebInspector.DataGridNode}
854     */
855    dataGridNodeFromNode: function(target)
856    {
857        var rowElement = target.enclosingNodeOrSelfWithNodeName("tr");
858        return rowElement && rowElement._dataGridNode;
859    },
860
861    /**
862     * @param {!Node} target
863     * @return {?string}
864     */
865    columnIdentifierFromNode: function(target)
866    {
867        var cellElement = target.enclosingNodeOrSelfWithNodeName("td");
868        return cellElement && cellElement.columnIdentifier_;
869    },
870
871    _clickInHeaderCell: function(event)
872    {
873        var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
874        if (!cell || !cell.columnIdentifier || !cell.hasStyleClass("sortable"))
875            return;
876
877        var sortOrder = WebInspector.DataGrid.Order.Ascending;
878        if ((cell === this._sortColumnCell) && this.isSortOrderAscending())
879            sortOrder = WebInspector.DataGrid.Order.Descending;
880
881        if (this._sortColumnCell)
882            this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
883        this._sortColumnCell = cell;
884
885        cell.addStyleClass("sort-" + sortOrder);
886
887        this.dispatchEventToListeners(WebInspector.DataGrid.Events.SortingChanged);
888    },
889
890    /**
891     * @param {string} columnIdentifier
892     * @param {!WebInspector.DataGrid.Order} sortOrder
893     */
894    markColumnAsSortedBy: function(columnIdentifier, sortOrder)
895    {
896        if (this._sortColumnCell)
897            this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
898        this._sortColumnCell = this._headerTableHeaders[columnIdentifier];
899        this._sortColumnCell.addStyleClass("sort-" + sortOrder);
900    },
901
902    headerTableHeader: function(columnIdentifier)
903    {
904        return this._headerTableHeaders[columnIdentifier];
905    },
906
907    _mouseDownInDataTable: function(event)
908    {
909        var gridNode = this.dataGridNodeFromNode(event.target);
910        if (!gridNode || !gridNode.selectable)
911            return;
912
913        if (gridNode.isEventWithinDisclosureTriangle(event))
914            return;
915
916        if (event.metaKey) {
917            if (gridNode.selected)
918                gridNode.deselect();
919            else
920                gridNode.select();
921        } else
922            gridNode.select();
923    },
924
925    _contextMenuInDataTable: function(event)
926    {
927        var contextMenu = new WebInspector.ContextMenu(event);
928
929        var gridNode = this.dataGridNodeFromNode(event.target);
930        if (this._refreshCallback && (!gridNode || gridNode !== this.creationNode))
931            contextMenu.appendItem(WebInspector.UIString("Refresh"), this._refreshCallback.bind(this));
932
933        if (gridNode && gridNode.selectable && !gridNode.isEventWithinDisclosureTriangle(event)) {
934            // FIXME: Use the column names for Editing, instead of just "Edit".
935            if (this._editCallback) {
936                if (gridNode === this.creationNode)
937                    contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add new" : "Add New"), this._startEditing.bind(this, event.target));
938                else {
939                    var columnIdentifier = this.columnIdentifierFromNode(event.target);
940                    if (columnIdentifier && this.columns[columnIdentifier].editable)
941                        contextMenu.appendItem(WebInspector.UIString("Edit"), this._startEditing.bind(this, event.target));
942                }
943            }
944            if (this._deleteCallback && gridNode !== this.creationNode)
945                contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode));
946            if (this._contextMenuCallback)
947                this._contextMenuCallback(contextMenu, gridNode);
948        }
949
950        contextMenu.show();
951    },
952
953    _clickInDataTable: function(event)
954    {
955        var gridNode = this.dataGridNodeFromNode(event.target);
956        if (!gridNode || !gridNode.hasChildren)
957            return;
958
959        if (!gridNode.isEventWithinDisclosureTriangle(event))
960            return;
961
962        if (gridNode.expanded) {
963            if (event.altKey)
964                gridNode.collapseRecursively();
965            else
966                gridNode.collapse();
967        } else {
968            if (event.altKey)
969                gridNode.expandRecursively();
970            else
971                gridNode.expand();
972        }
973    },
974
975    get resizeMethod()
976    {
977        if (typeof this._resizeMethod === "undefined")
978            return WebInspector.DataGrid.ResizeMethod.Nearest;
979        return this._resizeMethod;
980    },
981
982    set resizeMethod(method)
983    {
984        this._resizeMethod = method;
985    },
986
987    /**
988     * @return {boolean}
989     */
990    _startResizerDragging: function(event)
991    {
992        this._currentResizer = event.target;
993        return !!this._currentResizer.rightNeighboringColumnIndex
994    },
995
996    _resizerDragging: function(event)
997    {
998        var resizer = this._currentResizer;
999        if (!resizer)
1000            return;
1001
1002        var tableWidth = this._dataTable.offsetWidth; // Cache it early, before we invalidate layout.
1003
1004        // Constrain the dragpoint to be within the containing div of the
1005        // datagrid.
1006        var dragPoint = event.clientX - this.element.totalOffsetLeft();
1007        // Constrain the dragpoint to be within the space made up by the
1008        // column directly to the left and the column directly to the right.
1009        var leftCellIndex = resizer.leftNeighboringColumnIndex;
1010        var rightCellIndex = resizer.rightNeighboringColumnIndex;
1011        var firstRowCells = this.headerTableBody.rows[0].cells;
1012        var leftEdgeOfPreviousColumn = 0;
1013        for (var i = 0; i < leftCellIndex; i++)
1014            leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth;
1015
1016        // Differences for other resize methods
1017        if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.Last) {
1018            rightCellIndex = this.resizers.length;
1019        } else if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.First) {
1020            leftEdgeOfPreviousColumn += firstRowCells[leftCellIndex].offsetWidth - firstRowCells[0].offsetWidth;
1021            leftCellIndex = 0;
1022        }
1023
1024        var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[leftCellIndex].offsetWidth + firstRowCells[rightCellIndex].offsetWidth;
1025
1026        // Give each column some padding so that they don't disappear.
1027        var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding;
1028        var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding;
1029
1030        dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum);
1031
1032        resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px";
1033
1034        var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / tableWidth) * 100) + "%";
1035        this._headerTableColumnGroup.children[leftCellIndex].style.width = percentLeftColumn;
1036        this._dataTableColumnGroup.children[leftCellIndex].style.width = percentLeftColumn;
1037
1038        var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / tableWidth) * 100) + "%";
1039        this._headerTableColumnGroup.children[rightCellIndex].style.width =  percentRightColumn;
1040        this._dataTableColumnGroup.children[rightCellIndex].style.width = percentRightColumn;
1041
1042        var leftColumn = this._columnsArray[leftCellIndex];
1043        var rightColumn = this._columnsArray[rightCellIndex];
1044        if (leftColumn.weight || rightColumn.weight) {
1045            var sumOfWeights = leftColumn.weight + rightColumn.weight;
1046            var delta = rightEdgeOfNextColumn - leftEdgeOfPreviousColumn;
1047            leftColumn.weight = (dragPoint - leftEdgeOfPreviousColumn) * sumOfWeights / delta;
1048            rightColumn.weight = (rightEdgeOfNextColumn - dragPoint) * sumOfWeights / delta;
1049        }
1050
1051        this._positionResizers();
1052        event.preventDefault();
1053        this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
1054    },
1055
1056    _endResizerDragging: function(event)
1057    {
1058        this._currentResizer = null;
1059        this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
1060    },
1061
1062    ColumnResizePadding: 10,
1063
1064    CenterResizerOverBorderAdjustment: 3,
1065
1066    __proto__: WebInspector.View.prototype
1067}
1068
1069WebInspector.DataGrid.ResizeMethod = {
1070    Nearest: "nearest",
1071    First: "first",
1072    Last: "last"
1073}
1074
1075/**
1076 * @constructor
1077 * @extends {WebInspector.Object}
1078 * @param {*=} data
1079 * @param {boolean=} hasChildren
1080 */
1081WebInspector.DataGridNode = function(data, hasChildren)
1082{
1083    this._expanded = false;
1084    this._selected = false;
1085    this._shouldRefreshChildren = true;
1086    this._data = data || {};
1087    this.hasChildren = hasChildren || false;
1088    /** @type {!Array.<WebInspector.DataGridNode>} */
1089    this.children = [];
1090    this.dataGrid = null;
1091    this.parent = null;
1092    /** @type {WebInspector.DataGridNode} */
1093    this.previousSibling = null;
1094    /** @type {WebInspector.DataGridNode} */
1095    this.nextSibling = null;
1096    this.disclosureToggleWidth = 10;
1097}
1098
1099WebInspector.DataGridNode.prototype = {
1100    /** @type {boolean} */
1101    selectable: true,
1102
1103    /** @type {boolean} */
1104    _isRoot: false,
1105
1106    get element()
1107    {
1108        if (this._element)
1109            return this._element;
1110
1111        if (!this.dataGrid)
1112            return null;
1113
1114        this._element = document.createElement("tr");
1115        this._element._dataGridNode = this;
1116
1117        if (this.hasChildren)
1118            this._element.addStyleClass("parent");
1119        if (this.expanded)
1120            this._element.addStyleClass("expanded");
1121        if (this.selected)
1122            this._element.addStyleClass("selected");
1123        if (this.revealed)
1124            this._element.addStyleClass("revealed");
1125
1126        this.createCells();
1127        this._element.createChild("td", "corner");
1128
1129        return this._element;
1130    },
1131
1132    createCells: function()
1133    {
1134        var columnsArray = this.dataGrid._columnsArray;
1135        for (var i = 0; i < columnsArray.length; ++i) {
1136            var cell = this.createCell(columnsArray[i].identifier);
1137            this._element.appendChild(cell);
1138        }
1139    },
1140
1141    get data()
1142    {
1143        return this._data;
1144    },
1145
1146    set data(x)
1147    {
1148        this._data = x || {};
1149        this.refresh();
1150    },
1151
1152    get revealed()
1153    {
1154        if ("_revealed" in this)
1155            return this._revealed;
1156
1157        var currentAncestor = this.parent;
1158        while (currentAncestor && !currentAncestor._isRoot) {
1159            if (!currentAncestor.expanded) {
1160                this._revealed = false;
1161                return false;
1162            }
1163
1164            currentAncestor = currentAncestor.parent;
1165        }
1166
1167        this._revealed = true;
1168        return true;
1169    },
1170
1171    set hasChildren(x)
1172    {
1173        if (this._hasChildren === x)
1174            return;
1175
1176        this._hasChildren = x;
1177
1178        if (!this._element)
1179            return;
1180
1181        this._element.enableStyleClass("parent", this._hasChildren);
1182        this._element.enableStyleClass("expanded", this._hasChildren && this.expanded);
1183    },
1184
1185    get hasChildren()
1186    {
1187        return this._hasChildren;
1188    },
1189
1190    set revealed(x)
1191    {
1192        if (this._revealed === x)
1193            return;
1194
1195        this._revealed = x;
1196
1197        if (this._element)
1198            this._element.enableStyleClass("revealed", this._revealed);
1199
1200        for (var i = 0; i < this.children.length; ++i)
1201            this.children[i].revealed = x && this.expanded;
1202    },
1203
1204    get depth()
1205    {
1206        if ("_depth" in this)
1207            return this._depth;
1208        if (this.parent && !this.parent._isRoot)
1209            this._depth = this.parent.depth + 1;
1210        else
1211            this._depth = 0;
1212        return this._depth;
1213    },
1214
1215    get leftPadding()
1216    {
1217        if (typeof this._leftPadding === "number")
1218            return this._leftPadding;
1219
1220        this._leftPadding = this.depth * this.dataGrid.indentWidth;
1221        return this._leftPadding;
1222    },
1223
1224    get shouldRefreshChildren()
1225    {
1226        return this._shouldRefreshChildren;
1227    },
1228
1229    set shouldRefreshChildren(x)
1230    {
1231        this._shouldRefreshChildren = x;
1232        if (x && this.expanded)
1233            this.expand();
1234    },
1235
1236    get selected()
1237    {
1238        return this._selected;
1239    },
1240
1241    set selected(x)
1242    {
1243        if (x)
1244            this.select();
1245        else
1246            this.deselect();
1247    },
1248
1249    get expanded()
1250    {
1251        return this._expanded;
1252    },
1253
1254    set expanded(x)
1255    {
1256        if (x)
1257            this.expand();
1258        else
1259            this.collapse();
1260    },
1261
1262    refresh: function()
1263    {
1264        if (!this._element || !this.dataGrid)
1265            return;
1266
1267        this._element.removeChildren();
1268        this.createCells();
1269        this._element.createChild("td", "corner");
1270    },
1271
1272    /**
1273     * @param {string} columnIdentifier
1274     * @return {!Element}
1275     */
1276    createTD: function(columnIdentifier)
1277    {
1278        var cell = document.createElement("td");
1279        cell.className = columnIdentifier + "-column";
1280        cell.columnIdentifier_ = columnIdentifier;
1281
1282        var alignment = this.dataGrid.columns[columnIdentifier].align;
1283        if (alignment)
1284            cell.addStyleClass(alignment);
1285
1286        return cell;
1287    },
1288
1289    /**
1290     * @param {string} columnIdentifier
1291     * @return {!Element}
1292     */
1293    createCell: function(columnIdentifier)
1294    {
1295        var cell = this.createTD(columnIdentifier);
1296
1297        var data = this.data[columnIdentifier];
1298        var div = document.createElement("div");
1299        if (data instanceof Node)
1300            div.appendChild(data);
1301        else
1302            div.textContent = data;
1303        cell.appendChild(div);
1304
1305        if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
1306            cell.addStyleClass("disclosure");
1307            if (this.leftPadding)
1308                cell.style.setProperty("padding-left", this.leftPadding + "px");
1309        }
1310
1311        return cell;
1312    },
1313
1314    /**
1315     * @return {number}
1316     */
1317    nodeHeight: function()
1318    {
1319        var rowHeight = 16;
1320        if (!this.revealed)
1321            return 0;
1322        if (!this.expanded)
1323            return rowHeight;
1324        var result = rowHeight;
1325        for (var i = 0; i < this.children.length; i++)
1326            result += this.children[i].nodeHeight();
1327        return result;
1328    },
1329
1330    /**
1331     * @param {WebInspector.DataGridNode} child
1332     */
1333    appendChild: function(child)
1334    {
1335        this.insertChild(child, this.children.length);
1336    },
1337
1338    /**
1339     * @param {WebInspector.DataGridNode} child
1340     * @param {number} index
1341     */
1342    insertChild: function(child, index)
1343    {
1344        if (!child)
1345            throw("insertChild: Node can't be undefined or null.");
1346        if (child.parent === this)
1347            throw("insertChild: Node is already a child of this node.");
1348
1349        if (child.parent)
1350            child.parent.removeChild(child);
1351
1352        this.children.splice(index, 0, child);
1353        this.hasChildren = true;
1354
1355        child.parent = this;
1356        child.dataGrid = this.dataGrid;
1357        child._recalculateSiblings(index);
1358
1359        delete child._depth;
1360        delete child._revealed;
1361        delete child._attached;
1362        child._shouldRefreshChildren = true;
1363
1364        var current = child.children[0];
1365        while (current) {
1366            current.dataGrid = this.dataGrid;
1367            delete current._depth;
1368            delete current._revealed;
1369            delete current._attached;
1370            current._shouldRefreshChildren = true;
1371            current = current.traverseNextNode(false, child, true);
1372        }
1373
1374        if (this.expanded)
1375            child._attach();
1376        if (!this.revealed)
1377            child.revealed = false;
1378    },
1379
1380    /**
1381     * @param {WebInspector.DataGridNode} child
1382     */
1383    removeChild: function(child)
1384    {
1385        if (!child)
1386            throw("removeChild: Node can't be undefined or null.");
1387        if (child.parent !== this)
1388            throw("removeChild: Node is not a child of this node.");
1389
1390        child.deselect();
1391        child._detach();
1392
1393        this.children.remove(child, true);
1394
1395        if (child.previousSibling)
1396            child.previousSibling.nextSibling = child.nextSibling;
1397        if (child.nextSibling)
1398            child.nextSibling.previousSibling = child.previousSibling;
1399
1400        child.dataGrid = null;
1401        child.parent = null;
1402        child.nextSibling = null;
1403        child.previousSibling = null;
1404
1405        if (this.children.length <= 0)
1406            this.hasChildren = false;
1407    },
1408
1409    removeChildren: function()
1410    {
1411        for (var i = 0; i < this.children.length; ++i) {
1412            var child = this.children[i];
1413            child.deselect();
1414            child._detach();
1415
1416            child.dataGrid = null;
1417            child.parent = null;
1418            child.nextSibling = null;
1419            child.previousSibling = null;
1420        }
1421
1422        this.children = [];
1423        this.hasChildren = false;
1424    },
1425
1426    _recalculateSiblings: function(myIndex)
1427    {
1428        if (!this.parent)
1429            return;
1430
1431        var previousChild = (myIndex > 0 ? this.parent.children[myIndex - 1] : null);
1432
1433        if (previousChild) {
1434            previousChild.nextSibling = this;
1435            this.previousSibling = previousChild;
1436        } else
1437            this.previousSibling = null;
1438
1439        var nextChild = this.parent.children[myIndex + 1];
1440
1441        if (nextChild) {
1442            nextChild.previousSibling = this;
1443            this.nextSibling = nextChild;
1444        } else
1445            this.nextSibling = null;
1446    },
1447
1448    collapse: function()
1449    {
1450        if (this._isRoot)
1451            return;
1452        if (this._element)
1453            this._element.removeStyleClass("expanded");
1454
1455        this._expanded = false;
1456
1457        for (var i = 0; i < this.children.length; ++i)
1458            this.children[i].revealed = false;
1459    },
1460
1461    collapseRecursively: function()
1462    {
1463        var item = this;
1464        while (item) {
1465            if (item.expanded)
1466                item.collapse();
1467            item = item.traverseNextNode(false, this, true);
1468        }
1469    },
1470
1471    populate: function() { },
1472
1473    expand: function()
1474    {
1475        if (!this.hasChildren || this.expanded)
1476            return;
1477        if (this._isRoot)
1478            return;
1479
1480        if (this.revealed && !this._shouldRefreshChildren)
1481            for (var i = 0; i < this.children.length; ++i)
1482                this.children[i].revealed = true;
1483
1484        if (this._shouldRefreshChildren) {
1485            for (var i = 0; i < this.children.length; ++i)
1486                this.children[i]._detach();
1487
1488            this.populate();
1489
1490            if (this._attached) {
1491                for (var i = 0; i < this.children.length; ++i) {
1492                    var child = this.children[i];
1493                    if (this.revealed)
1494                        child.revealed = true;
1495                    child._attach();
1496                }
1497            }
1498
1499            delete this._shouldRefreshChildren;
1500        }
1501
1502        if (this._element)
1503            this._element.addStyleClass("expanded");
1504
1505        this._expanded = true;
1506    },
1507
1508    expandRecursively: function()
1509    {
1510        var item = this;
1511        while (item) {
1512            item.expand();
1513            item = item.traverseNextNode(false, this);
1514        }
1515    },
1516
1517    reveal: function()
1518    {
1519        if (this._isRoot)
1520            return;
1521        var currentAncestor = this.parent;
1522        while (currentAncestor && !currentAncestor._isRoot) {
1523            if (!currentAncestor.expanded)
1524                currentAncestor.expand();
1525            currentAncestor = currentAncestor.parent;
1526        }
1527
1528        this.element.scrollIntoViewIfNeeded(false);
1529    },
1530
1531    /**
1532     * @param {boolean=} supressSelectedEvent
1533     */
1534    select: function(supressSelectedEvent)
1535    {
1536        if (!this.dataGrid || !this.selectable || this.selected)
1537            return;
1538
1539        if (this.dataGrid.selectedNode)
1540            this.dataGrid.selectedNode.deselect();
1541
1542        this._selected = true;
1543        this.dataGrid.selectedNode = this;
1544
1545        if (this._element)
1546            this._element.addStyleClass("selected");
1547
1548        if (!supressSelectedEvent)
1549            this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Events.SelectedNode);
1550    },
1551
1552    revealAndSelect: function()
1553    {
1554        if (this._isRoot)
1555            return;
1556        this.reveal();
1557        this.select();
1558    },
1559
1560    /**
1561     * @param {boolean=} supressDeselectedEvent
1562     */
1563    deselect: function(supressDeselectedEvent)
1564    {
1565        if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
1566            return;
1567
1568        this._selected = false;
1569        this.dataGrid.selectedNode = null;
1570
1571        if (this._element)
1572            this._element.removeStyleClass("selected");
1573
1574        if (!supressDeselectedEvent)
1575            this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Events.DeselectedNode);
1576    },
1577
1578    /**
1579     * @param {boolean} skipHidden
1580     * @param {WebInspector.DataGridNode=} stayWithin
1581     * @param {boolean=} dontPopulate
1582     * @param {Object=} info
1583     * @return {WebInspector.DataGridNode}
1584     */
1585    traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info)
1586    {
1587        if (!dontPopulate && this.hasChildren)
1588            this.populate();
1589
1590        if (info)
1591            info.depthChange = 0;
1592
1593        var node = (!skipHidden || this.revealed) ? this.children[0] : null;
1594        if (node && (!skipHidden || this.expanded)) {
1595            if (info)
1596                info.depthChange = 1;
1597            return node;
1598        }
1599
1600        if (this === stayWithin)
1601            return null;
1602
1603        node = (!skipHidden || this.revealed) ? this.nextSibling : null;
1604        if (node)
1605            return node;
1606
1607        node = this;
1608        while (node && !node._isRoot && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) {
1609            if (info)
1610                info.depthChange -= 1;
1611            node = node.parent;
1612        }
1613
1614        if (!node)
1615            return null;
1616
1617        return (!skipHidden || node.revealed) ? node.nextSibling : null;
1618    },
1619
1620    /**
1621     * @param {boolean} skipHidden
1622     * @param {boolean=} dontPopulate
1623     * @return {WebInspector.DataGridNode}
1624     */
1625    traversePreviousNode: function(skipHidden, dontPopulate)
1626    {
1627        var node = (!skipHidden || this.revealed) ? this.previousSibling : null;
1628        if (!dontPopulate && node && node.hasChildren)
1629            node.populate();
1630
1631        while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null)) {
1632            if (!dontPopulate && node.hasChildren)
1633                node.populate();
1634            node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null);
1635        }
1636
1637        if (node)
1638            return node;
1639
1640        if (!this.parent || this.parent._isRoot)
1641            return null;
1642
1643        return this.parent;
1644    },
1645
1646    /**
1647     * @return {boolean}
1648     */
1649    isEventWithinDisclosureTriangle: function(event)
1650    {
1651        if (!this.hasChildren)
1652            return false;
1653        var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
1654        if (!cell.hasStyleClass("disclosure"))
1655            return false;
1656
1657        var left = cell.totalOffsetLeft() + this.leftPadding;
1658        return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth;
1659    },
1660
1661    _attach: function()
1662    {
1663        if (!this.dataGrid || this._attached)
1664            return;
1665
1666        this._attached = true;
1667
1668        var nextNode = null;
1669        var previousNode = this.traversePreviousNode(true, true);
1670        if (previousNode && previousNode.element.parentNode && previousNode.element.nextSibling)
1671            nextNode = previousNode.element.nextSibling;
1672        if (!nextNode)
1673            nextNode = this.dataGrid.dataTableBody.firstChild;
1674        this.dataGrid.dataTableBody.insertBefore(this.element, nextNode);
1675
1676        if (this.expanded)
1677            for (var i = 0; i < this.children.length; ++i)
1678                this.children[i]._attach();
1679    },
1680
1681    _detach: function()
1682    {
1683        if (!this._attached)
1684            return;
1685
1686        this._attached = false;
1687
1688        if (this._element && this._element.parentNode)
1689            this._element.parentNode.removeChild(this._element);
1690
1691        for (var i = 0; i < this.children.length; ++i)
1692            this.children[i]._detach();
1693
1694        this.wasDetached();
1695    },
1696
1697    wasDetached: function()
1698    {
1699    },
1700
1701    savePosition: function()
1702    {
1703        if (this._savedPosition)
1704            return;
1705
1706        if (!this.parent)
1707            throw("savePosition: Node must have a parent.");
1708        this._savedPosition = {
1709            parent: this.parent,
1710            index: this.parent.children.indexOf(this)
1711        };
1712    },
1713
1714    restorePosition: function()
1715    {
1716        if (!this._savedPosition)
1717            return;
1718
1719        if (this.parent !== this._savedPosition.parent)
1720            this._savedPosition.parent.insertChild(this, this._savedPosition.index);
1721
1722        delete this._savedPosition;
1723    },
1724
1725    __proto__: WebInspector.Object.prototype
1726}
1727
1728/**
1729 * @constructor
1730 * @extends {WebInspector.DataGridNode}
1731 */
1732WebInspector.CreationDataGridNode = function(data, hasChildren)
1733{
1734    WebInspector.DataGridNode.call(this, data, hasChildren);
1735    this.isCreationNode = true;
1736}
1737
1738WebInspector.CreationDataGridNode.prototype = {
1739    makeNormal: function()
1740    {
1741        delete this.isCreationNode;
1742        delete this.makeNormal;
1743    },
1744
1745    __proto__: WebInspector.DataGridNode.prototype
1746}
1747