• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
26WebInspector.DataGrid = function(columns, editCallback, deleteCallback)
27{
28    this.element = document.createElement("div");
29    this.element.className = "data-grid";
30    this.element.tabIndex = 0;
31    this.element.addEventListener("keydown", this._keyDown.bind(this), false);
32
33    this._headerTable = document.createElement("table");
34    this._headerTable.className = "header";
35
36    this._dataTable = document.createElement("table");
37    this._dataTable.className = "data";
38
39    this._dataTable.addEventListener("mousedown", this._mouseDownInDataTable.bind(this), true);
40    this._dataTable.addEventListener("click", this._clickInDataTable.bind(this), true);
41
42    this._dataTable.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true);
43
44    // FIXME: Add a createCallback which is different from editCallback and has different
45    // behavior when creating a new node.
46    if (editCallback) {
47        this._dataTable.addEventListener("dblclick", this._ondblclick.bind(this), false);
48        this._editCallback = editCallback;
49    }
50    if (deleteCallback)
51        this._deleteCallback = deleteCallback;
52
53    this.aligned = {};
54
55    var scrollContainer = document.createElement("div");
56    scrollContainer.className = "data-container";
57    scrollContainer.appendChild(this._dataTable);
58
59    this.element.appendChild(this._headerTable);
60    this.element.appendChild(scrollContainer);
61
62    var headerRow = document.createElement("tr");
63    var columnGroup = document.createElement("colgroup");
64    this._columnCount = 0;
65
66    for (var columnIdentifier in columns) {
67        var column = columns[columnIdentifier];
68        if (column.disclosure)
69            this.disclosureColumnIdentifier = columnIdentifier;
70
71        var col = document.createElement("col");
72        if (column.width)
73            col.style.width = column.width;
74        column.element = col;
75        columnGroup.appendChild(col);
76
77        var cell = document.createElement("th");
78        cell.className = columnIdentifier + "-column";
79        cell.columnIdentifier = columnIdentifier;
80
81        var div = document.createElement("div");
82        div.textContent = column.title;
83        cell.appendChild(div);
84
85        if (column.sort) {
86            cell.addStyleClass("sort-" + column.sort);
87            this._sortColumnCell = cell;
88        }
89
90        if (column.sortable) {
91            cell.addEventListener("click", this._clickInHeaderCell.bind(this), false);
92            cell.addStyleClass("sortable");
93        }
94
95        if (column.aligned) {
96            cell.addStyleClass(column.aligned);
97            this.aligned[columnIdentifier] = column.aligned;
98        }
99
100        headerRow.appendChild(cell);
101
102        ++this._columnCount;
103    }
104
105    columnGroup.span = this._columnCount;
106
107    var cell = document.createElement("th");
108    cell.className = "corner";
109    headerRow.appendChild(cell);
110
111    this._headerTableColumnGroup = columnGroup;
112    this._headerTable.appendChild(this._headerTableColumnGroup);
113    this.headerTableBody.appendChild(headerRow);
114
115    var fillerRow = document.createElement("tr");
116    fillerRow.className = "filler";
117
118    for (var i = 0; i < this._columnCount; ++i) {
119        var cell = document.createElement("td");
120        fillerRow.appendChild(cell);
121    }
122
123    this._dataTableColumnGroup = columnGroup.cloneNode(true);
124    this._dataTable.appendChild(this._dataTableColumnGroup);
125    this.dataTableBody.appendChild(fillerRow);
126
127    this.columns = columns || {};
128    this.children = [];
129    this.selectedNode = null;
130    this.expandNodesWhenArrowing = false;
131    this.root = true;
132    this.hasChildren = false;
133    this.expanded = true;
134    this.revealed = true;
135    this.selected = false;
136    this.dataGrid = this;
137    this.indentWidth = 15;
138    this.resizers = [];
139    this.columnWidthsInitialized = false;
140}
141
142WebInspector.DataGrid.prototype = {
143    _ondblclick: function(event)
144    {
145        if (this._editing || this._editingNode)
146            return;
147
148        this._startEditing(event.target);
149    },
150
151    _startEditingColumnOfDataGridNode: function(node, column)
152    {
153        this._editing = true;
154        this._editingNode = node;
155        this._editingNode.select();
156
157        var element = this._editingNode._element.children[column];
158        WebInspector.startEditing(element, this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent);
159        window.getSelection().setBaseAndExtent(element, 0, element, 1);
160    },
161
162    _startEditing: function(target)
163    {
164        var element = target.enclosingNodeOrSelfWithNodeName("td");
165        if (!element)
166            return;
167
168        this._editingNode = this.dataGridNodeFromNode(target);
169        if (!this._editingNode) {
170            if (!this.creationNode)
171                return;
172            this._editingNode = this.creationNode;
173        }
174
175        // Force editing the 1st column when editing the creation node
176        if (this._editingNode.isCreationNode)
177            return this._startEditingColumnOfDataGridNode(this._editingNode, 0);
178
179        this._editing = true;
180        WebInspector.startEditing(element, this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent);
181        window.getSelection().setBaseAndExtent(element, 0, element, 1);
182    },
183
184    _editingCommitted: function(element, newText, oldText, context, moveDirection)
185    {
186        // FIXME: We need more column identifiers here throughout this function.
187        // Not needed yet since only editable DataGrid is DOM Storage, which is Key - Value.
188
189        // FIXME: Better way to do this than regular expressions?
190        var columnIdentifier = parseInt(element.className.match(/\b(\d+)-column\b/)[1]);
191
192        var textBeforeEditing = this._editingNode.data[columnIdentifier];
193        var currentEditingNode = this._editingNode;
194
195        function moveToNextIfNeeded(wasChange) {
196            if (!moveDirection)
197                return;
198
199            if (moveDirection === "forward") {
200                if (currentEditingNode.isCreationNode && columnIdentifier === 0 && !wasChange)
201                    return;
202
203                if (columnIdentifier === 0)
204                    return this._startEditingColumnOfDataGridNode(currentEditingNode, 1);
205
206                var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true);
207                if (nextDataGridNode)
208                    return this._startEditingColumnOfDataGridNode(nextDataGridNode, 0);
209                if (currentEditingNode.isCreationNode && wasChange) {
210                    addCreationNode(false);
211                    return this._startEditingColumnOfDataGridNode(this.creationNode, 0);
212                }
213                return;
214            }
215
216            if (moveDirection === "backward") {
217                if (columnIdentifier === 1)
218                    return this._startEditingColumnOfDataGridNode(currentEditingNode, 0);
219                    var nextDataGridNode = currentEditingNode.traversePreviousNode(true, null, true);
220
221                if (nextDataGridNode)
222                    return this._startEditingColumnOfDataGridNode(nextDataGridNode, 1);
223                return;
224            }
225        }
226
227        if (textBeforeEditing == newText) {
228            this._editingCancelled(element);
229            moveToNextIfNeeded.call(this, false);
230            return;
231        }
232
233        // Update the text in the datagrid that we typed
234        this._editingNode.data[columnIdentifier] = newText;
235
236        // Make the callback - expects an editing node (table row), the column number that is being edited,
237        // the text that used to be there, and the new text.
238        this._editCallback(this._editingNode, columnIdentifier, textBeforeEditing, newText);
239
240        if (this._editingNode.isCreationNode)
241            this.addCreationNode(false);
242
243        this._editingCancelled(element);
244        moveToNextIfNeeded.call(this, true);
245    },
246
247    _editingCancelled: function(element, context)
248    {
249        delete this._editing;
250        this._editingNode = null;
251    },
252
253    get sortColumnIdentifier()
254    {
255        if (!this._sortColumnCell)
256            return null;
257        return this._sortColumnCell.columnIdentifier;
258    },
259
260    get sortOrder()
261    {
262        if (!this._sortColumnCell || this._sortColumnCell.hasStyleClass("sort-ascending"))
263            return "ascending";
264        if (this._sortColumnCell.hasStyleClass("sort-descending"))
265            return "descending";
266        return null;
267    },
268
269    get headerTableBody()
270    {
271        if ("_headerTableBody" in this)
272            return this._headerTableBody;
273
274        this._headerTableBody = this._headerTable.getElementsByTagName("tbody")[0];
275        if (!this._headerTableBody) {
276            this._headerTableBody = this.element.ownerDocument.createElement("tbody");
277            this._headerTable.insertBefore(this._headerTableBody, this._headerTable.tFoot);
278        }
279
280        return this._headerTableBody;
281    },
282
283    get dataTableBody()
284    {
285        if ("_dataTableBody" in this)
286            return this._dataTableBody;
287
288        this._dataTableBody = this._dataTable.getElementsByTagName("tbody")[0];
289        if (!this._dataTableBody) {
290            this._dataTableBody = this.element.ownerDocument.createElement("tbody");
291            this._dataTable.insertBefore(this._dataTableBody, this._dataTable.tFoot);
292        }
293
294        return this._dataTableBody;
295    },
296
297    autoSizeColumns: function(minPercent, maxPercent)
298    {
299        if (minPercent)
300            minPercent = Math.min(minPercent, Math.floor(100 / this._columnCount));
301        var widths = {};
302        var columns = this.columns;
303        for (var columnIdentifier in columns)
304            widths[columnIdentifier] = (columns[columnIdentifier].title || "").length;
305
306        for (var i = 0; i < this.children.length; ++i) {
307            var node = this.children[i];
308            for (var columnIdentifier in columns) {
309                var text = node.data[columnIdentifier] || "";
310                if (text.length > widths[columnIdentifier])
311                    widths[columnIdentifier] = text.length;
312            }
313        }
314
315        var totalColumnWidths = 0;
316        for (var columnIdentifier in columns)
317            totalColumnWidths += widths[columnIdentifier];
318
319        var recoupPercent = 0;
320        for (var columnIdentifier in columns) {
321            var width = Math.round(100 * widths[columnIdentifier] / totalColumnWidths);
322            if (minPercent && width < minPercent) {
323                recoupPercent += (minPercent - width);
324                width = minPercent;
325            } else if (maxPercent && width > maxPercent) {
326                recoupPercent -= (width - maxPercent);
327                width = maxPercent;
328            }
329            widths[columnIdentifier] = width;
330        }
331
332        while (minPercent && recoupPercent > 0) {
333            for (var columnIdentifier in columns) {
334                if (widths[columnIdentifier] > minPercent) {
335                    --widths[columnIdentifier];
336                    --recoupPercent;
337                    if (!recoupPercent)
338                        break;
339                }
340            }
341        }
342
343        while (maxPercent && recoupPercent < 0) {
344            for (var columnIdentifier in columns) {
345                if (widths[columnIdentifier] < maxPercent) {
346                    ++widths[columnIdentifier];
347                    ++recoupPercent;
348                    if (!recoupPercent)
349                        break;
350                }
351            }
352        }
353
354        for (var columnIdentifier in columns)
355            columns[columnIdentifier].element.style.width = widths[columnIdentifier] + "%";
356        this.columnWidthsInitialized = false;
357        this.updateWidths();
358    },
359
360    // Updates the widths of the table, including the positions of the column
361    // resizers.
362    //
363    // IMPORTANT: This function MUST be called once after the element of the
364    // DataGrid is attached to its parent element and every subsequent time the
365    // width of the parent element is changed in order to make it possible to
366    // resize the columns.
367    //
368    // If this function is not called after the DataGrid is attached to its
369    // parent element, then the DataGrid's columns will not be resizable.
370    updateWidths: function()
371    {
372        var headerTableColumns = this._headerTableColumnGroup.children;
373
374        var left = 0;
375        var tableWidth = this._dataTable.offsetWidth;
376        var numColumns = headerTableColumns.length;
377
378        if (!this.columnWidthsInitialized) {
379            // Give all the columns initial widths now so that during a resize,
380            // when the two columns that get resized get a percent value for
381            // their widths, all the other columns already have percent values
382            // for their widths.
383            for (var i = 0; i < numColumns; i++) {
384                var columnWidth = this.headerTableBody.rows[0].cells[i].offsetWidth;
385                var percentWidth = ((columnWidth / tableWidth) * 100) + "%";
386                this._headerTableColumnGroup.children[i].style.width = percentWidth;
387                this._dataTableColumnGroup.children[i].style.width = percentWidth;
388            }
389            this.columnWidthsInitialized = true;
390        }
391
392        // Make n - 1 resizers for n columns.
393        for (var i = 0; i < numColumns - 1; i++) {
394            var resizer = this.resizers[i];
395
396            if (!resizer) {
397                // This is the first call to updateWidth, so the resizers need
398                // to be created.
399                resizer = document.createElement("div");
400                resizer.addStyleClass("data-grid-resizer");
401                // This resizer is associated with the column to its right.
402                resizer.rightNeighboringColumnID = i + 1;
403                resizer.addEventListener("mousedown", this._startResizerDragging.bind(this), false);
404                this.element.appendChild(resizer);
405                this.resizers[i] = resizer;
406            }
407
408            // Get the width of the cell in the first (and only) row of the
409            // header table in order to determine the width of the column, since
410            // it is not possible to query a column for its width.
411            left += this.headerTableBody.rows[0].cells[i].offsetWidth;
412
413            resizer.style.left = left + "px";
414        }
415    },
416
417    addCreationNode: function(hasChildren)
418    {
419        if (this.creationNode)
420            this.creationNode.makeNormal();
421
422        var emptyData = {};
423        for (var column in this.columns)
424            emptyData[column] = '';
425        this.creationNode = new WebInspector.CreationDataGridNode(emptyData, hasChildren);
426        this.appendChild(this.creationNode);
427    },
428
429    appendChild: function(child)
430    {
431        this.insertChild(child, this.children.length);
432    },
433
434    insertChild: function(child, index)
435    {
436        if (!child)
437            throw("insertChild: Node can't be undefined or null.");
438        if (child.parent === this)
439            throw("insertChild: Node is already a child of this node.");
440
441        if (child.parent)
442            child.parent.removeChild(child);
443
444        this.children.splice(index, 0, child);
445        this.hasChildren = true;
446
447        child.parent = this;
448        child.dataGrid = this.dataGrid;
449        child._recalculateSiblings(index);
450
451        delete child._depth;
452        delete child._revealed;
453        delete child._attached;
454        child._shouldRefreshChildren = true;
455
456        var current = child.children[0];
457        while (current) {
458            current.dataGrid = this.dataGrid;
459            delete current._depth;
460            delete current._revealed;
461            delete current._attached;
462            current._shouldRefreshChildren = true;
463            current = current.traverseNextNode(false, child, true);
464        }
465
466        if (this.expanded)
467            child._attach();
468    },
469
470    removeChild: function(child)
471    {
472        if (!child)
473            throw("removeChild: Node can't be undefined or null.");
474        if (child.parent !== this)
475            throw("removeChild: Node is not a child of this node.");
476
477        child.deselect();
478
479        this.children.remove(child, true);
480
481        if (child.previousSibling)
482            child.previousSibling.nextSibling = child.nextSibling;
483        if (child.nextSibling)
484            child.nextSibling.previousSibling = child.previousSibling;
485
486        child.dataGrid = null;
487        child.parent = null;
488        child.nextSibling = null;
489        child.previousSibling = null;
490
491        if (this.children.length <= 0)
492            this.hasChildren = false;
493    },
494
495    removeChildren: function()
496    {
497        for (var i = 0; i < this.children.length; ++i) {
498            var child = this.children[i];
499            child.deselect();
500            child._detach();
501
502            child.dataGrid = null;
503            child.parent = null;
504            child.nextSibling = null;
505            child.previousSibling = null;
506        }
507
508        this.children = [];
509        this.hasChildren = false;
510    },
511
512    removeChildrenRecursive: function()
513    {
514        var childrenToRemove = this.children;
515
516        var child = this.children[0];
517        while (child) {
518            if (child.children.length)
519                childrenToRemove = childrenToRemove.concat(child.children);
520            child = child.traverseNextNode(false, this, true);
521        }
522
523        for (var i = 0; i < childrenToRemove.length; ++i) {
524            var child = childrenToRemove[i];
525            child.deselect();
526            child._detach();
527
528            child.children = [];
529            child.dataGrid = null;
530            child.parent = null;
531            child.nextSibling = null;
532            child.previousSibling = null;
533        }
534
535        this.children = [];
536    },
537
538
539    _keyDown: function(event)
540    {
541        if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing)
542            return;
543
544        var handled = false;
545        var nextSelectedNode;
546        if (event.keyIdentifier === "Up" && !event.altKey) {
547            nextSelectedNode = this.selectedNode.traversePreviousNode(true);
548            while (nextSelectedNode && !nextSelectedNode.selectable)
549                nextSelectedNode = nextSelectedNode.traversePreviousNode(!this.expandTreeNodesWhenArrowing);
550            handled = nextSelectedNode ? true : false;
551        } else if (event.keyIdentifier === "Down" && !event.altKey) {
552            nextSelectedNode = this.selectedNode.traverseNextNode(true);
553            while (nextSelectedNode && !nextSelectedNode.selectable)
554                nextSelectedNode = nextSelectedNode.traverseNextNode(!this.expandTreeNodesWhenArrowing);
555            handled = nextSelectedNode ? true : false;
556        } else if (event.keyIdentifier === "Left") {
557            if (this.selectedNode.expanded) {
558                if (event.altKey)
559                    this.selectedNode.collapseRecursively();
560                else
561                    this.selectedNode.collapse();
562                handled = true;
563            } else if (this.selectedNode.parent && !this.selectedNode.parent.root) {
564                handled = true;
565                if (this.selectedNode.parent.selectable) {
566                    nextSelectedNode = this.selectedNode.parent;
567                    handled = nextSelectedNode ? true : false;
568                } else if (this.selectedNode.parent)
569                    this.selectedNode.parent.collapse();
570            }
571        } else if (event.keyIdentifier === "Right") {
572            if (!this.selectedNode.revealed) {
573                this.selectedNode.reveal();
574                handled = true;
575            } else if (this.selectedNode.hasChildren) {
576                handled = true;
577                if (this.selectedNode.expanded) {
578                    nextSelectedNode = this.selectedNode.children[0];
579                    handled = nextSelectedNode ? true : false;
580                } else {
581                    if (event.altKey)
582                        this.selectedNode.expandRecursively();
583                    else
584                        this.selectedNode.expand();
585                }
586            }
587        } else if (event.keyCode === 8 || event.keyCode === 46) {
588            if (this._deleteCallback) {
589                handled = true;
590                this._deleteCallback(this.selectedNode);
591            }
592        } else if (isEnterKey(event)) {
593            if (this._editCallback) {
594                handled = true;
595                // The first child of the selected element is the <td class="0-column">,
596                // and that's what we want to edit.
597                this._startEditing(this.selectedNode._element.children[0]);
598            }
599        }
600
601        if (nextSelectedNode) {
602            nextSelectedNode.reveal();
603            nextSelectedNode.select();
604        }
605
606        if (handled) {
607            event.preventDefault();
608            event.stopPropagation();
609        }
610    },
611
612    expand: function()
613    {
614        // This is the root, do nothing.
615    },
616
617    collapse: function()
618    {
619        // This is the root, do nothing.
620    },
621
622    reveal: function()
623    {
624        // This is the root, do nothing.
625    },
626
627    dataGridNodeFromNode: function(target)
628    {
629        var rowElement = target.enclosingNodeOrSelfWithNodeName("tr");
630        return rowElement._dataGridNode;
631    },
632
633    dataGridNodeFromPoint: function(x, y)
634    {
635        var node = this._dataTable.ownerDocument.elementFromPoint(x, y);
636        var rowElement = node.enclosingNodeOrSelfWithNodeName("tr");
637        return rowElement._dataGridNode;
638    },
639
640    _clickInHeaderCell: function(event)
641    {
642        var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
643        if (!cell || !cell.columnIdentifier || !cell.hasStyleClass("sortable"))
644            return;
645
646        var sortOrder = this.sortOrder;
647
648        if (this._sortColumnCell) {
649            this._sortColumnCell.removeStyleClass("sort-ascending");
650            this._sortColumnCell.removeStyleClass("sort-descending");
651        }
652
653        if (cell == this._sortColumnCell) {
654            if (sortOrder == "ascending")
655                sortOrder = "descending";
656            else
657                sortOrder = "ascending";
658        }
659
660        this._sortColumnCell = cell;
661
662        cell.addStyleClass("sort-" + sortOrder);
663
664        this.dispatchEventToListeners("sorting changed");
665    },
666
667    _mouseDownInDataTable: function(event)
668    {
669        var gridNode = this.dataGridNodeFromNode(event.target);
670        if (!gridNode || !gridNode.selectable)
671            return;
672
673        if (gridNode.isEventWithinDisclosureTriangle(event))
674            return;
675
676        if (event.metaKey) {
677            if (gridNode.selected)
678                gridNode.deselect();
679            else
680                gridNode.select();
681        } else
682            gridNode.select();
683    },
684
685    _contextMenuInDataTable: function(event)
686    {
687        var gridNode = this.dataGridNodeFromNode(event.target);
688        if (!gridNode || !gridNode.selectable)
689            return;
690
691        if (gridNode.isEventWithinDisclosureTriangle(event))
692            return;
693
694        var contextMenu = new WebInspector.ContextMenu();
695
696        // FIXME: Use the column names for Editing, instead of just "Edit".
697        if (this.dataGrid._editCallback) {
698            if (gridNode === this.creationNode)
699                contextMenu.appendItem(WebInspector.UIString("Add New"), this._startEditing.bind(this, event.target));
700            else
701                contextMenu.appendItem(WebInspector.UIString("Edit"), this._startEditing.bind(this, event.target));
702        }
703        if (this.dataGrid._deleteCallback && gridNode !== this.creationNode)
704            contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode));
705
706        contextMenu.show(event);
707    },
708
709    _clickInDataTable: function(event)
710    {
711        var gridNode = this.dataGridNodeFromNode(event.target);
712        if (!gridNode || !gridNode.hasChildren)
713            return;
714
715        if (!gridNode.isEventWithinDisclosureTriangle(event))
716            return;
717
718        if (gridNode.expanded) {
719            if (event.altKey)
720                gridNode.collapseRecursively();
721            else
722                gridNode.collapse();
723        } else {
724            if (event.altKey)
725                gridNode.expandRecursively();
726            else
727                gridNode.expand();
728        }
729    },
730
731    _startResizerDragging: function(event)
732    {
733        this.currentResizer = event.target;
734        if (!this.currentResizer.rightNeighboringColumnID)
735            return;
736        WebInspector.elementDragStart(this.lastResizer, this._resizerDragging.bind(this),
737            this._endResizerDragging.bind(this), event, "col-resize");
738    },
739
740    _resizerDragging: function(event)
741    {
742        var resizer = this.currentResizer;
743        if (!resizer)
744            return;
745
746        // Constrain the dragpoint to be within the containing div of the
747        // datagrid.
748        var dragPoint = event.clientX - this.element.totalOffsetLeft;
749        // Constrain the dragpoint to be within the space made up by the
750        // column directly to the left and the column directly to the right.
751        var leftEdgeOfPreviousColumn = 0;
752        var firstRowCells = this.headerTableBody.rows[0].cells;
753        for (var i = 0; i < resizer.rightNeighboringColumnID - 1; i++)
754            leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth;
755
756        var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[resizer.rightNeighboringColumnID - 1].offsetWidth + firstRowCells[resizer.rightNeighboringColumnID].offsetWidth;
757
758        // Give each column some padding so that they don't disappear.
759        var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding;
760        var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding;
761
762        dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum);
763
764        resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px";
765
766        var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / this._dataTable.offsetWidth) * 100) + "%";
767        this._headerTableColumnGroup.children[resizer.rightNeighboringColumnID - 1].style.width = percentLeftColumn;
768        this._dataTableColumnGroup.children[resizer.rightNeighboringColumnID - 1].style.width = percentLeftColumn;
769
770        var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / this._dataTable.offsetWidth) * 100) + "%";
771        this._headerTableColumnGroup.children[resizer.rightNeighboringColumnID].style.width =  percentRightColumn;
772        this._dataTableColumnGroup.children[resizer.rightNeighboringColumnID].style.width = percentRightColumn;
773
774        event.preventDefault();
775    },
776
777    _endResizerDragging: function(event)
778    {
779        WebInspector.elementDragEnd(event);
780        this.currentResizer = null;
781    },
782
783    ColumnResizePadding: 10,
784
785    CenterResizerOverBorderAdjustment: 3,
786}
787
788WebInspector.DataGrid.prototype.__proto__ = WebInspector.Object.prototype;
789
790WebInspector.DataGridNode = function(data, hasChildren)
791{
792    this._expanded = false;
793    this._selected = false;
794    this._shouldRefreshChildren = true;
795    this._data = data || {};
796    this.hasChildren = hasChildren || false;
797    this.children = [];
798    this.dataGrid = null;
799    this.parent = null;
800    this.previousSibling = null;
801    this.nextSibling = null;
802    this.disclosureToggleWidth = 10;
803}
804
805WebInspector.DataGridNode.prototype = {
806    selectable: true,
807
808    get element()
809    {
810        if (this._element)
811            return this._element;
812
813        if (!this.dataGrid)
814            return null;
815
816        this._element = document.createElement("tr");
817        this._element._dataGridNode = this;
818
819        if (this.hasChildren)
820            this._element.addStyleClass("parent");
821        if (this.expanded)
822            this._element.addStyleClass("expanded");
823        if (this.selected)
824            this._element.addStyleClass("selected");
825        if (this.revealed)
826            this._element.addStyleClass("revealed");
827
828        for (var columnIdentifier in this.dataGrid.columns) {
829            var cell = this.createCell(columnIdentifier);
830            this._element.appendChild(cell);
831        }
832
833        return this._element;
834    },
835
836    get data()
837    {
838        return this._data;
839    },
840
841    set data(x)
842    {
843        this._data = x || {};
844        this.refresh();
845    },
846
847    get revealed()
848    {
849        if ("_revealed" in this)
850            return this._revealed;
851
852        var currentAncestor = this.parent;
853        while (currentAncestor && !currentAncestor.root) {
854            if (!currentAncestor.expanded) {
855                this._revealed = false;
856                return false;
857            }
858
859            currentAncestor = currentAncestor.parent;
860        }
861
862        this._revealed = true;
863        return true;
864    },
865
866    set hasChildren(x)
867    {
868        if (this._hasChildren === x)
869            return;
870
871        this._hasChildren = x;
872
873        if (!this._element)
874            return;
875
876        if (this._hasChildren)
877        {
878            this._element.addStyleClass("parent");
879            if (this.expanded)
880                this._element.addStyleClass("expanded");
881        }
882        else
883        {
884            this._element.removeStyleClass("parent");
885            this._element.removeStyleClass("expanded");
886        }
887    },
888
889    get hasChildren()
890    {
891        return this._hasChildren;
892    },
893
894    set revealed(x)
895    {
896        if (this._revealed === x)
897            return;
898
899        this._revealed = x;
900
901        if (this._element) {
902            if (this._revealed)
903                this._element.addStyleClass("revealed");
904            else
905                this._element.removeStyleClass("revealed");
906        }
907
908        for (var i = 0; i < this.children.length; ++i)
909            this.children[i].revealed = x && this.expanded;
910    },
911
912    get depth()
913    {
914        if ("_depth" in this)
915            return this._depth;
916        if (this.parent && !this.parent.root)
917            this._depth = this.parent.depth + 1;
918        else
919            this._depth = 0;
920        return this._depth;
921    },
922
923    get shouldRefreshChildren()
924    {
925        return this._shouldRefreshChildren;
926    },
927
928    set shouldRefreshChildren(x)
929    {
930        this._shouldRefreshChildren = x;
931        if (x && this.expanded)
932            this.expand();
933    },
934
935    get selected()
936    {
937        return this._selected;
938    },
939
940    set selected(x)
941    {
942        if (x)
943            this.select();
944        else
945            this.deselect();
946    },
947
948    get expanded()
949    {
950        return this._expanded;
951    },
952
953    set expanded(x)
954    {
955        if (x)
956            this.expand();
957        else
958            this.collapse();
959    },
960
961    refresh: function()
962    {
963        if (!this._element || !this.dataGrid)
964            return;
965
966        this._element.removeChildren();
967
968        for (var columnIdentifier in this.dataGrid.columns) {
969            var cell = this.createCell(columnIdentifier);
970            this._element.appendChild(cell);
971        }
972    },
973
974    createCell: function(columnIdentifier)
975    {
976        var cell = document.createElement("td");
977        cell.className = columnIdentifier + "-column";
978
979        var alignment = this.dataGrid.aligned[columnIdentifier];
980        if (alignment)
981            cell.addStyleClass(alignment);
982
983        var div = document.createElement("div");
984        div.textContent = this.data[columnIdentifier];
985        cell.appendChild(div);
986
987        if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
988            cell.addStyleClass("disclosure");
989            if (this.depth)
990                cell.style.setProperty("padding-left", (this.depth * this.dataGrid.indentWidth) + "px");
991        }
992
993        return cell;
994    },
995
996    // Share these functions with DataGrid. They are written to work with a DataGridNode this object.
997    appendChild: WebInspector.DataGrid.prototype.appendChild,
998    insertChild: WebInspector.DataGrid.prototype.insertChild,
999    removeChild: WebInspector.DataGrid.prototype.removeChild,
1000    removeChildren: WebInspector.DataGrid.prototype.removeChildren,
1001    removeChildrenRecursive: WebInspector.DataGrid.prototype.removeChildrenRecursive,
1002
1003    _recalculateSiblings: function(myIndex)
1004    {
1005        if (!this.parent)
1006            return;
1007
1008        var previousChild = (myIndex > 0 ? this.parent.children[myIndex - 1] : null);
1009
1010        if (previousChild) {
1011            previousChild.nextSibling = this;
1012            this.previousSibling = previousChild;
1013        } else
1014            this.previousSibling = null;
1015
1016        var nextChild = this.parent.children[myIndex + 1];
1017
1018        if (nextChild) {
1019            nextChild.previousSibling = this;
1020            this.nextSibling = nextChild;
1021        } else
1022            this.nextSibling = null;
1023    },
1024
1025    collapse: function()
1026    {
1027        if (this._element)
1028            this._element.removeStyleClass("expanded");
1029
1030        this._expanded = false;
1031
1032        for (var i = 0; i < this.children.length; ++i)
1033            this.children[i].revealed = false;
1034
1035        this.dispatchEventToListeners("collapsed");
1036    },
1037
1038    collapseRecursively: function()
1039    {
1040        var item = this;
1041        while (item) {
1042            if (item.expanded)
1043                item.collapse();
1044            item = item.traverseNextNode(false, this, true);
1045        }
1046    },
1047
1048    expand: function()
1049    {
1050        if (!this.hasChildren || this.expanded)
1051            return;
1052
1053        if (this.revealed && !this._shouldRefreshChildren)
1054            for (var i = 0; i < this.children.length; ++i)
1055                this.children[i].revealed = true;
1056
1057        if (this._shouldRefreshChildren) {
1058            for (var i = 0; i < this.children.length; ++i)
1059                this.children[i]._detach();
1060
1061            this.dispatchEventToListeners("populate");
1062
1063            if (this._attached) {
1064                for (var i = 0; i < this.children.length; ++i) {
1065                    var child = this.children[i];
1066                    if (this.revealed)
1067                        child.revealed = true;
1068                    child._attach();
1069                }
1070            }
1071
1072            delete this._shouldRefreshChildren;
1073        }
1074
1075        if (this._element)
1076            this._element.addStyleClass("expanded");
1077
1078        this._expanded = true;
1079
1080        this.dispatchEventToListeners("expanded");
1081    },
1082
1083    expandRecursively: function()
1084    {
1085        var item = this;
1086        while (item) {
1087            item.expand();
1088            item = item.traverseNextNode(false, this);
1089        }
1090    },
1091
1092    reveal: function()
1093    {
1094        var currentAncestor = this.parent;
1095        while (currentAncestor && !currentAncestor.root) {
1096            if (!currentAncestor.expanded)
1097                currentAncestor.expand();
1098            currentAncestor = currentAncestor.parent;
1099        }
1100
1101        this.element.scrollIntoViewIfNeeded(false);
1102
1103        this.dispatchEventToListeners("revealed");
1104    },
1105
1106    select: function(supressSelectedEvent)
1107    {
1108        if (!this.dataGrid || !this.selectable || this.selected)
1109            return;
1110
1111        if (this.dataGrid.selectedNode)
1112            this.dataGrid.selectedNode.deselect();
1113
1114        this._selected = true;
1115        this.dataGrid.selectedNode = this;
1116
1117        if (this._element)
1118            this._element.addStyleClass("selected");
1119
1120        if (!supressSelectedEvent)
1121            this.dispatchEventToListeners("selected");
1122    },
1123
1124    deselect: function(supressDeselectedEvent)
1125    {
1126        if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
1127            return;
1128
1129        this._selected = false;
1130        this.dataGrid.selectedNode = null;
1131
1132        if (this._element)
1133            this._element.removeStyleClass("selected");
1134
1135        if (!supressDeselectedEvent)
1136            this.dispatchEventToListeners("deselected");
1137    },
1138
1139    traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info)
1140    {
1141        if (!dontPopulate && this.hasChildren)
1142            this.dispatchEventToListeners("populate");
1143
1144        if (info)
1145            info.depthChange = 0;
1146
1147        var node = (!skipHidden || this.revealed) ? this.children[0] : null;
1148        if (node && (!skipHidden || this.expanded)) {
1149            if (info)
1150                info.depthChange = 1;
1151            return node;
1152        }
1153
1154        if (this === stayWithin)
1155            return null;
1156
1157        node = (!skipHidden || this.revealed) ? this.nextSibling : null;
1158        if (node)
1159            return node;
1160
1161        node = this;
1162        while (node && !node.root && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) {
1163            if (info)
1164                info.depthChange -= 1;
1165            node = node.parent;
1166        }
1167
1168        if (!node)
1169            return null;
1170
1171        return (!skipHidden || node.revealed) ? node.nextSibling : null;
1172    },
1173
1174    traversePreviousNode: function(skipHidden, dontPopulate)
1175    {
1176        var node = (!skipHidden || this.revealed) ? this.previousSibling : null;
1177        if (!dontPopulate && node && node.hasChildren)
1178            node.dispatchEventToListeners("populate");
1179
1180        while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null)) {
1181            if (!dontPopulate && node.hasChildren)
1182                node.dispatchEventToListeners("populate");
1183            node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null);
1184        }
1185
1186        if (node)
1187            return node;
1188
1189        if (!this.parent || this.parent.root)
1190            return null;
1191
1192        return this.parent;
1193    },
1194
1195    isEventWithinDisclosureTriangle: function(event)
1196    {
1197        if (!this.hasChildren)
1198            return false;
1199        var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
1200        if (!cell.hasStyleClass("disclosure"))
1201            return false;
1202        var computedLeftPadding = window.getComputedStyle(cell).getPropertyCSSValue("padding-left").getFloatValue(CSSPrimitiveValue.CSS_PX);
1203        var left = cell.totalOffsetLeft + computedLeftPadding;
1204        return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth;
1205    },
1206
1207    _attach: function()
1208    {
1209        if (!this.dataGrid || this._attached)
1210            return;
1211
1212        this._attached = true;
1213
1214        var nextNode = null;
1215        var previousNode = this.traversePreviousNode(true, true);
1216        if (previousNode && previousNode.element.parentNode && previousNode.element.nextSibling)
1217            var nextNode = previousNode.element.nextSibling;
1218        if (!nextNode)
1219            nextNode = this.dataGrid.dataTableBody.lastChild;
1220        this.dataGrid.dataTableBody.insertBefore(this.element, nextNode);
1221
1222        if (this.expanded)
1223            for (var i = 0; i < this.children.length; ++i)
1224                this.children[i]._attach();
1225    },
1226
1227    _detach: function()
1228    {
1229        if (!this._attached)
1230            return;
1231
1232        this._attached = false;
1233
1234        if (this._element && this._element.parentNode)
1235            this._element.parentNode.removeChild(this._element);
1236
1237        for (var i = 0; i < this.children.length; ++i)
1238            this.children[i]._detach();
1239    },
1240
1241    savePosition: function()
1242    {
1243        if (this._savedPosition)
1244            return;
1245
1246        if (!this.parent)
1247            throw("savePosition: Node must have a parent.");
1248        this._savedPosition = {
1249            parent: this.parent,
1250            index: this.parent.children.indexOf(this)
1251        };
1252    },
1253
1254    restorePosition: function()
1255    {
1256        if (!this._savedPosition)
1257            return;
1258
1259        if (this.parent !== this._savedPosition.parent)
1260            this._savedPosition.parent.insertChild(this, this._savedPosition.index);
1261
1262        delete this._savedPosition;
1263    }
1264}
1265
1266WebInspector.DataGridNode.prototype.__proto__ = WebInspector.Object.prototype;
1267
1268WebInspector.CreationDataGridNode = function(data, hasChildren)
1269{
1270    WebInspector.DataGridNode.call(this, data, hasChildren);
1271    this.isCreationNode = true;
1272}
1273
1274WebInspector.CreationDataGridNode.prototype = {
1275    makeNormal: function()
1276    {
1277        delete this.isCreationNode;
1278        delete this.makeNormal;
1279    }
1280}
1281
1282WebInspector.CreationDataGridNode.prototype.__proto__ = WebInspector.DataGridNode.prototype;
1283