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