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