2 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
28 * @extends {WebInspector.View}
29 * @param {function(WebInspector.DataGridNode, number, string, string)=} editCallback
30 * @param {function(WebInspector.DataGridNode)=} deleteCallback
32 WebInspector.DataGrid = function(columns, editCallback, deleteCallback)
34 WebInspector.View.call(this);
35 this.registerRequiredCSS("dataGrid.css");
37 this.element.className = "data-grid";
38 this.element.tabIndex = 0;
39 this.element.addEventListener("keydown", this._keyDown.bind(this), false);
41 this._headerTable = document.createElement("table");
42 this._headerTable.className = "header";
43 this._headerTableHeaders = {};
45 this._dataTable = document.createElement("table");
46 this._dataTable.className = "data";
48 this._dataTable.addEventListener("mousedown", this._mouseDownInDataTable.bind(this), true);
49 this._dataTable.addEventListener("click", this._clickInDataTable.bind(this), true);
51 this._dataTable.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true);
53 // FIXME: Add a createCallback which is different from editCallback and has different
54 // behavior when creating a new node.
56 this._dataTable.addEventListener("dblclick", this._ondblclick.bind(this), false);
57 this._editCallback = editCallback;
60 this._deleteCallback = deleteCallback;
64 this._scrollContainer = document.createElement("div");
65 this._scrollContainer.className = "data-container";
66 this._scrollContainer.appendChild(this._dataTable);
68 this.element.appendChild(this._headerTable);
69 this.element.appendChild(this._scrollContainer);
71 var headerRow = document.createElement("tr");
72 var columnGroup = document.createElement("colgroup");
73 this._columnCount = 0;
75 for (var columnIdentifier in columns) {
76 var column = columns[columnIdentifier];
77 if (column.disclosure)
78 this.disclosureColumnIdentifier = columnIdentifier;
80 var col = document.createElement("col");
82 col.style.width = column.width;
84 columnGroup.appendChild(col);
86 var cell = document.createElement("th");
87 cell.className = columnIdentifier + "-column";
88 cell.columnIdentifier = columnIdentifier;
89 this._headerTableHeaders[columnIdentifier] = cell;
91 var div = document.createElement("div");
92 if (column.titleDOMFragment)
93 div.appendChild(column.titleDOMFragment);
95 div.textContent = column.title;
96 cell.appendChild(div);
99 cell.addStyleClass("sort-" + column.sort);
100 this._sortColumnCell = cell;
103 if (column.sortable) {
104 cell.addEventListener("click", this._clickInHeaderCell.bind(this), false);
105 cell.addStyleClass("sortable");
109 this.aligned[columnIdentifier] = column.aligned;
111 headerRow.appendChild(cell);
116 columnGroup.span = this._columnCount;
118 var cell = document.createElement("th");
119 cell.className = "corner";
120 headerRow.appendChild(cell);
122 this._headerTableColumnGroup = columnGroup;
123 this._headerTable.appendChild(this._headerTableColumnGroup);
124 this.headerTableBody.appendChild(headerRow);
126 var fillerRow = document.createElement("tr");
127 fillerRow.className = "filler";
129 for (var columnIdentifier in columns) {
130 var column = columns[columnIdentifier];
131 var td = document.createElement("td");
132 td.className = columnIdentifier + "-column";
133 fillerRow.appendChild(td);
136 this._dataTableColumnGroup = columnGroup.cloneNode(true);
137 this._dataTable.appendChild(this._dataTableColumnGroup);
138 this.dataTableBody.appendChild(fillerRow);
140 this.columns = columns || {};
141 this._columnsArray = [];
142 for (var columnIdentifier in columns) {
143 columns[columnIdentifier].ordinal = this._columnsArray.length;
144 columns[columnIdentifier].identifier = columnIdentifier;
145 this._columnsArray.push(columns[columnIdentifier]);
148 for (var i = 0; i < this._columnsArray.length; ++i)
149 this._columnsArray[i].bodyElement = this._dataTableColumnGroup.children[i];
152 this.selectedNode = null;
153 this.expandNodesWhenArrowing = false;
155 this.hasChildren = false;
156 this.expanded = true;
157 this.revealed = true;
158 this.selected = false;
159 this.dataGrid = this;
160 this.indentWidth = 15;
162 this._columnWidthsInitialized = false;
166 * @param {Array.<string>} columnNames
167 * @param {Array.<string>} values
169 WebInspector.DataGrid.createSortableDataGrid = function(columnNames, values)
171 var numColumns = columnNames.length;
177 for (var i = 0; i < columnNames.length; ++i) {
179 column.width = columnNames[i].length;
180 column.title = columnNames[i];
181 column.sortable = true;
183 columns[columnNames[i]] = column;
187 for (var i = 0; i < values.length / numColumns; ++i) {
189 for (var j = 0; j < columnNames.length; ++j)
190 data[columnNames[j]] = values[numColumns * i + j];
192 var node = new WebInspector.DataGridNode(data, false);
193 node.selectable = false;
197 var dataGrid = new WebInspector.DataGrid(columns);
198 var length = nodes.length;
199 for (var i = 0; i < length; ++i)
200 dataGrid.appendChild(nodes[i]);
202 dataGrid.addEventListener("sorting changed", sortDataGrid, this);
204 function sortDataGrid()
206 var nodes = dataGrid.children.slice();
207 var sortColumnIdentifier = dataGrid.sortColumnIdentifier;
208 var sortDirection = dataGrid.sortOrder === "ascending" ? 1 : -1;
209 var columnIsNumeric = true;
211 for (var i = 0; i < nodes.length; i++) {
212 if (isNaN(Number(nodes[i].data[sortColumnIdentifier])))
213 columnIsNumeric = false;
216 function comparator(dataGridNode1, dataGridNode2)
218 var item1 = dataGridNode1.data[sortColumnIdentifier];
219 var item2 = dataGridNode2.data[sortColumnIdentifier];
222 if (columnIsNumeric) {
223 // Sort numbers based on comparing their values rather than a lexicographical comparison.
224 var number1 = parseFloat(item1);
225 var number2 = parseFloat(item2);
226 comparison = number1 < number2 ? -1 : (number1 > number2 ? 1 : 0);
228 comparison = item1 < item2 ? -1 : (item1 > item2 ? 1 : 0);
230 return sortDirection * comparison;
233 nodes.sort(comparator);
234 dataGrid.removeChildren();
235 for (var i = 0; i < nodes.length; i++)
236 dataGrid.appendChild(nodes[i]);
241 WebInspector.DataGrid.prototype = {
242 get refreshCallback()
244 return this._refreshCallback;
247 set refreshCallback(refreshCallback)
249 this._refreshCallback = refreshCallback;
252 _ondblclick: function(event)
254 if (this._editing || this._editingNode)
257 this._startEditing(event.target);
260 _startEditingColumnOfDataGridNode: function(node, column)
262 this._editing = true;
263 this._editingNode = node;
264 this._editingNode.select();
266 var element = this._editingNode._element.children[column];
267 WebInspector.startEditing(element, this._startEditingConfig(element));
268 window.getSelection().setBaseAndExtent(element, 0, element, 1);
271 _startEditing: function(target)
273 var element = target.enclosingNodeOrSelfWithNodeName("td");
277 this._editingNode = this.dataGridNodeFromNode(target);
278 if (!this._editingNode) {
279 if (!this.creationNode)
281 this._editingNode = this.creationNode;
284 // Force editing the 1st column when editing the creation node
285 if (this._editingNode.isCreationNode)
286 return this._startEditingColumnOfDataGridNode(this._editingNode, 0);
288 this._editing = true;
289 WebInspector.startEditing(element, this._startEditingConfig(element));
291 window.getSelection().setBaseAndExtent(element, 0, element, 1);
295 _startEditingConfig: function(element)
297 return new WebInspector.EditingConfig(this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent);
300 _editingCommitted: function(element, newText, oldText, context, moveDirection)
302 // FIXME: We need more column identifiers here throughout this function.
303 // Not needed yet since only editable DataGrid is DOM Storage, which is Key - Value.
305 // FIXME: Better way to do this than regular expressions?
306 var columnIdentifier = parseInt(element.className.match(/\b(\d+)-column\b/)[1], 10);
308 var textBeforeEditing = this._editingNode.data[columnIdentifier];
309 var currentEditingNode = this._editingNode;
311 function moveToNextIfNeeded(wasChange) {
315 if (moveDirection === "forward") {
316 if (currentEditingNode.isCreationNode && columnIdentifier === 0 && !wasChange)
319 if (columnIdentifier === 0)
320 return this._startEditingColumnOfDataGridNode(currentEditingNode, 1);
322 var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true);
323 if (nextDataGridNode)
324 return this._startEditingColumnOfDataGridNode(nextDataGridNode, 0);
325 if (currentEditingNode.isCreationNode && wasChange) {
326 this.addCreationNode(false);
327 return this._startEditingColumnOfDataGridNode(this.creationNode, 0);
332 if (moveDirection === "backward") {
333 if (columnIdentifier === 1)
334 return this._startEditingColumnOfDataGridNode(currentEditingNode, 0);
335 var nextDataGridNode = currentEditingNode.traversePreviousNode(true, null, true);
337 if (nextDataGridNode)
338 return this._startEditingColumnOfDataGridNode(nextDataGridNode, 1);
343 if (textBeforeEditing == newText) {
344 this._editingCancelled(element);
345 moveToNextIfNeeded.call(this, false);
349 // Update the text in the datagrid that we typed
350 this._editingNode.data[columnIdentifier] = newText;
352 // Make the callback - expects an editing node (table row), the column number that is being edited,
353 // the text that used to be there, and the new text.
354 this._editCallback(this._editingNode, columnIdentifier, textBeforeEditing, newText);
356 if (this._editingNode.isCreationNode)
357 this.addCreationNode(false);
359 this._editingCancelled(element);
360 moveToNextIfNeeded.call(this, true);
363 _editingCancelled: function(element)
365 delete this._editing;
366 this._editingNode = null;
369 get sortColumnIdentifier()
371 if (!this._sortColumnCell)
373 return this._sortColumnCell.columnIdentifier;
378 if (!this._sortColumnCell || this._sortColumnCell.hasStyleClass("sort-ascending"))
380 if (this._sortColumnCell.hasStyleClass("sort-descending"))
385 get headerTableBody()
387 if ("_headerTableBody" in this)
388 return this._headerTableBody;
390 this._headerTableBody = this._headerTable.getElementsByTagName("tbody")[0];
391 if (!this._headerTableBody) {
392 this._headerTableBody = this.element.ownerDocument.createElement("tbody");
393 this._headerTable.insertBefore(this._headerTableBody, this._headerTable.tFoot);
396 return this._headerTableBody;
401 if ("_dataTableBody" in this)
402 return this._dataTableBody;
404 this._dataTableBody = this._dataTable.getElementsByTagName("tbody")[0];
405 if (!this._dataTableBody) {
406 this._dataTableBody = this.element.ownerDocument.createElement("tbody");
407 this._dataTable.insertBefore(this._dataTableBody, this._dataTable.tFoot);
410 return this._dataTableBody;
414 * @param {number=} maxDescentLevel
416 autoSizeColumns: function(minPercent, maxPercent, maxDescentLevel)
419 minPercent = Math.min(minPercent, Math.floor(100 / this._columnCount));
421 var columns = this.columns;
422 for (var columnIdentifier in columns)
423 widths[columnIdentifier] = (columns[columnIdentifier].title || "").length;
425 var children = maxDescentLevel ? this._enumerateChildren(this, [], maxDescentLevel + 1) : this.children;
426 for (var i = 0; i < children.length; ++i) {
427 var node = children[i];
428 for (var columnIdentifier in columns) {
429 var text = node.data[columnIdentifier] || "";
430 if (text.length > widths[columnIdentifier])
431 widths[columnIdentifier] = text.length;
435 var totalColumnWidths = 0;
436 for (var columnIdentifier in columns)
437 totalColumnWidths += widths[columnIdentifier];
439 var recoupPercent = 0;
440 for (var columnIdentifier in columns) {
441 var width = Math.round(100 * widths[columnIdentifier] / totalColumnWidths);
442 if (minPercent && width < minPercent) {
443 recoupPercent += (minPercent - width);
445 } else if (maxPercent && width > maxPercent) {
446 recoupPercent -= (width - maxPercent);
449 widths[columnIdentifier] = width;
452 while (minPercent && recoupPercent > 0) {
453 for (var columnIdentifier in columns) {
454 if (widths[columnIdentifier] > minPercent) {
455 --widths[columnIdentifier];
463 while (maxPercent && recoupPercent < 0) {
464 for (var columnIdentifier in columns) {
465 if (widths[columnIdentifier] < maxPercent) {
466 ++widths[columnIdentifier];
474 for (var columnIdentifier in columns)
475 columns[columnIdentifier].element.style.width = widths[columnIdentifier] + "%";
476 this._columnWidthsInitialized = false;
480 _enumerateChildren: function(rootNode, result, maxLevel)
483 result.push(rootNode);
486 for (var i = 0; i < rootNode.children.length; ++i)
487 this._enumerateChildren(rootNode.children[i], result, maxLevel - 1);
496 // Updates the widths of the table, including the positions of the column
499 // IMPORTANT: This function MUST be called once after the element of the
500 // DataGrid is attached to its parent element and every subsequent time the
501 // width of the parent element is changed in order to make it possible to
502 // resize the columns.
504 // If this function is not called after the DataGrid is attached to its
505 // parent element, then the DataGrid's columns will not be resizable.
506 updateWidths: function()
508 var headerTableColumns = this._headerTableColumnGroup.children;
510 var tableWidth = this._dataTable.offsetWidth;
511 var numColumns = headerTableColumns.length;
513 // Do not attempt to use offsetes if we're not attached to the document tree yet.
514 if (!this._columnWidthsInitialized && this.element.offsetWidth) {
515 // Give all the columns initial widths now so that during a resize,
516 // when the two columns that get resized get a percent value for
517 // their widths, all the other columns already have percent values
519 for (var i = 0; i < numColumns; i++) {
520 var columnWidth = this.headerTableBody.rows[0].cells[i].offsetWidth;
521 var percentWidth = ((columnWidth / tableWidth) * 100) + "%";
522 this._headerTableColumnGroup.children[i].style.width = percentWidth;
523 this._dataTableColumnGroup.children[i].style.width = percentWidth;
525 this._columnWidthsInitialized = true;
527 this._positionResizers();
528 this.dispatchEventToListeners("width changed");
531 columnWidthsMap: function()
534 for (var i = 0; i < this._columnsArray.length; ++i) {
535 var width = this._headerTableColumnGroup.children[i].style.width;
536 result[this._columnsArray[i].columnIdentifier] = parseFloat(width);
541 applyColumnWidthsMap: function(columnWidthsMap)
543 for (var columnIdentifier in this.columns) {
544 var column = this.columns[columnIdentifier];
545 var width = (columnWidthsMap[columnIdentifier] || 0) + "%";
546 this._headerTableColumnGroup.children[column.ordinal].style.width = width;
547 this._dataTableColumnGroup.children[column.ordinal].style.width = width;
551 delete this._columnWidthsInitialized;
555 isColumnVisible: function(columnIdentifier)
557 var column = this.columns[columnIdentifier];
558 var columnElement = column.element;
559 return !columnElement.hidden;
562 showColumn: function(columnIdentifier)
564 var column = this.columns[columnIdentifier];
565 var columnElement = column.element;
566 if (!columnElement.hidden)
569 columnElement.hidden = false;
570 columnElement.removeStyleClass("hidden");
572 var columnBodyElement = column.bodyElement;
573 columnBodyElement.hidden = false;
574 columnBodyElement.removeStyleClass("hidden");
577 hideColumn: function(columnIdentifier)
579 var column = this.columns[columnIdentifier];
580 var columnElement = column.element;
581 if (columnElement.hidden)
584 var oldWidth = parseFloat(columnElement.style.width);
586 columnElement.hidden = true;
587 columnElement.addStyleClass("hidden");
588 columnElement.style.width = 0;
590 var columnBodyElement = column.bodyElement;
591 columnBodyElement.hidden = true;
592 columnBodyElement.addStyleClass("hidden");
593 columnBodyElement.style.width = 0;
595 this._columnWidthsInitialized = false;
598 get scrollContainer()
600 return this._scrollContainer;
603 isScrolledToLastRow: function()
605 return this._scrollContainer.isScrolledToBottom();
608 scrollToLastRow: function()
610 this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight - this._scrollContainer.offsetHeight;
613 _positionResizers: function()
615 var headerTableColumns = this._headerTableColumnGroup.children;
616 var numColumns = headerTableColumns.length;
618 var previousResizer = null;
620 // Make n - 1 resizers for n columns.
621 for (var i = 0; i < numColumns - 1; i++) {
622 var resizer = this.resizers[i];
625 // This is the first call to updateWidth, so the resizers need
627 resizer = document.createElement("div");
628 resizer.addStyleClass("data-grid-resizer");
629 // This resizer is associated with the column to its right.
630 resizer.addEventListener("mousedown", this._startResizerDragging.bind(this), false);
631 this.element.appendChild(resizer);
632 this.resizers[i] = resizer;
635 // Get the width of the cell in the first (and only) row of the
636 // header table in order to determine the width of the column, since
637 // it is not possible to query a column for its width.
638 left += this.headerTableBody.rows[0].cells[i].offsetWidth;
640 var columnIsVisible = !this._headerTableColumnGroup.children[i].hidden;
641 if (columnIsVisible) {
642 resizer.style.removeProperty("display");
643 resizer.style.left = left + "px";
644 resizer.leftNeighboringColumnID = i;
646 previousResizer.rightNeighboringColumnID = i;
647 previousResizer = resizer;
649 resizer.style.setProperty("display", "none");
650 resizer.leftNeighboringColumnID = 0;
651 resizer.rightNeighboringColumnID = 0;
655 previousResizer.rightNeighboringColumnID = numColumns - 1;
658 addCreationNode: function(hasChildren)
660 if (this.creationNode)
661 this.creationNode.makeNormal();
664 for (var column in this.columns)
665 emptyData[column] = '';
666 this.creationNode = new WebInspector.CreationDataGridNode(emptyData, hasChildren);
667 this.appendChild(this.creationNode);
670 appendChild: function(child)
672 this.insertChild(child, this.children.length);
675 insertChild: function(child, index)
678 throw("insertChild: Node can't be undefined or null.");
679 if (child.parent === this)
680 throw("insertChild: Node is already a child of this node.");
683 child.parent.removeChild(child);
685 this.children.splice(index, 0, child);
686 this.hasChildren = true;
689 child.dataGrid = this.dataGrid;
690 child._recalculateSiblings(index);
693 delete child._revealed;
694 delete child._attached;
695 child._shouldRefreshChildren = true;
697 var current = child.children[0];
699 current.dataGrid = this.dataGrid;
700 delete current._depth;
701 delete current._revealed;
702 delete current._attached;
703 current._shouldRefreshChildren = true;
704 current = current.traverseNextNode(false, child, true);
711 removeChild: function(child)
714 throw("removeChild: Node can't be undefined or null.");
715 if (child.parent !== this)
716 throw("removeChild: Node is not a child of this node.");
721 this.children.remove(child, true);
723 if (child.previousSibling)
724 child.previousSibling.nextSibling = child.nextSibling;
725 if (child.nextSibling)
726 child.nextSibling.previousSibling = child.previousSibling;
728 child.dataGrid = null;
730 child.nextSibling = null;
731 child.previousSibling = null;
733 if (this.children.length <= 0)
734 this.hasChildren = false;
737 removeChildren: function()
739 for (var i = 0; i < this.children.length; ++i) {
740 var child = this.children[i];
744 child.dataGrid = null;
746 child.nextSibling = null;
747 child.previousSibling = null;
751 this.hasChildren = false;
754 removeChildrenRecursive: function()
756 var childrenToRemove = this.children;
758 var child = this.children[0];
760 if (child.children.length)
761 childrenToRemove = childrenToRemove.concat(child.children);
762 child = child.traverseNextNode(false, this, true);
765 for (var i = 0; i < childrenToRemove.length; ++i) {
766 child = childrenToRemove[i];
771 child.dataGrid = null;
773 child.nextSibling = null;
774 child.previousSibling = null;
780 sortNodes: function(comparator, reverseMode)
782 function comparatorWrapper(a, b)
784 if (a._dataGridNode._data.summaryRow)
786 if (b._dataGridNode._data.summaryRow)
789 var aDataGirdNode = a._dataGridNode;
790 var bDataGirdNode = b._dataGridNode;
791 return reverseMode ? comparator(bDataGirdNode, aDataGirdNode) : comparator(aDataGirdNode, bDataGirdNode);
794 var tbody = this.dataTableBody;
795 var tbodyParent = tbody.parentElement;
796 tbodyParent.removeChild(tbody);
798 var childNodes = tbody.childNodes;
799 var fillerRow = childNodes[childNodes.length - 1];
801 var sortedRows = Array.prototype.slice.call(childNodes, 0, childNodes.length - 1);
802 sortedRows.sort(comparatorWrapper);
803 var sortedRowsLength = sortedRows.length;
805 tbody.removeChildren();
806 var previousSiblingNode = null;
807 for (var i = 0; i < sortedRowsLength; ++i) {
808 var row = sortedRows[i];
809 var node = row._dataGridNode;
810 node.previousSibling = previousSiblingNode;
811 if (previousSiblingNode)
812 previousSiblingNode.nextSibling = node;
813 tbody.appendChild(row);
814 previousSiblingNode = node;
816 if (previousSiblingNode)
817 previousSiblingNode.nextSibling = null;
819 tbody.appendChild(fillerRow);
820 tbodyParent.appendChild(tbody);
823 _keyDown: function(event)
825 if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing)
829 var nextSelectedNode;
830 if (event.keyIdentifier === "Up" && !event.altKey) {
831 nextSelectedNode = this.selectedNode.traversePreviousNode(true);
832 while (nextSelectedNode && !nextSelectedNode.selectable)
833 nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
834 handled = nextSelectedNode ? true : false;
835 } else if (event.keyIdentifier === "Down" && !event.altKey) {
836 nextSelectedNode = this.selectedNode.traverseNextNode(true);
837 while (nextSelectedNode && !nextSelectedNode.selectable)
838 nextSelectedNode = nextSelectedNode.traverseNextNode(true);
839 handled = nextSelectedNode ? true : false;
840 } else if (event.keyIdentifier === "Left") {
841 if (this.selectedNode.expanded) {
843 this.selectedNode.collapseRecursively();
845 this.selectedNode.collapse();
847 } else if (this.selectedNode.parent && !this.selectedNode.parent.root) {
849 if (this.selectedNode.parent.selectable) {
850 nextSelectedNode = this.selectedNode.parent;
851 handled = nextSelectedNode ? true : false;
852 } else if (this.selectedNode.parent)
853 this.selectedNode.parent.collapse();
855 } else if (event.keyIdentifier === "Right") {
856 if (!this.selectedNode.revealed) {
857 this.selectedNode.reveal();
859 } else if (this.selectedNode.hasChildren) {
861 if (this.selectedNode.expanded) {
862 nextSelectedNode = this.selectedNode.children[0];
863 handled = nextSelectedNode ? true : false;
866 this.selectedNode.expandRecursively();
868 this.selectedNode.expand();
871 } else if (event.keyCode === 8 || event.keyCode === 46) {
872 if (this._deleteCallback) {
874 this._deleteCallback(this.selectedNode);
876 } else if (isEnterKey(event)) {
877 if (this._editCallback) {
879 // The first child of the selected element is the <td class="0-column">,
880 // and that's what we want to edit.
881 this._startEditing(this.selectedNode._element.children[0]);
885 if (nextSelectedNode) {
886 nextSelectedNode.reveal();
887 nextSelectedNode.select();
891 event.preventDefault();
892 event.stopPropagation();
898 // This is the root, do nothing.
903 // This is the root, do nothing.
908 // This is the root, do nothing.
911 revealAndSelect: function()
913 // This is the root, do nothing.
916 dataGridNodeFromNode: function(target)
918 var rowElement = target.enclosingNodeOrSelfWithNodeName("tr");
919 return rowElement && rowElement._dataGridNode;
922 dataGridNodeFromPoint: function(x, y)
924 var node = this._dataTable.ownerDocument.elementFromPoint(x, y);
925 var rowElement = node.enclosingNodeOrSelfWithNodeName("tr");
926 return rowElement && rowElement._dataGridNode;
929 _clickInHeaderCell: function(event)
931 var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
932 if (!cell || !cell.columnIdentifier || !cell.hasStyleClass("sortable"))
935 var sortOrder = this.sortOrder;
937 if (this._sortColumnCell)
938 this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
940 if (cell == this._sortColumnCell) {
941 if (sortOrder === "ascending")
942 sortOrder = "descending";
944 sortOrder = "ascending";
947 this._sortColumnCell = cell;
949 cell.addStyleClass("sort-" + sortOrder);
951 this.dispatchEventToListeners("sorting changed");
954 markColumnAsSortedBy: function(columnIdentifier, sortOrder)
956 if (this._sortColumnCell)
957 this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
958 this._sortColumnCell = this._headerTableHeaders[columnIdentifier];
959 this._sortColumnCell.addStyleClass("sort-" + sortOrder);
962 headerTableHeader: function(columnIdentifier)
964 return this._headerTableHeaders[columnIdentifier];
967 _mouseDownInDataTable: function(event)
969 var gridNode = this.dataGridNodeFromNode(event.target);
970 if (!gridNode || !gridNode.selectable)
973 if (gridNode.isEventWithinDisclosureTriangle(event))
977 if (gridNode.selected)
985 _contextMenuInDataTable: function(event)
987 var contextMenu = new WebInspector.ContextMenu();
989 var gridNode = this.dataGridNodeFromNode(event.target);
990 if (this.dataGrid._refreshCallback && (!gridNode || gridNode !== this.creationNode))
991 contextMenu.appendItem(WebInspector.UIString("Refresh"), this._refreshCallback.bind(this));
993 if (gridNode && gridNode.selectable && !gridNode.isEventWithinDisclosureTriangle(event)) {
994 // FIXME: Use the column names for Editing, instead of just "Edit".
995 if (this.dataGrid._editCallback) {
996 if (gridNode === this.creationNode)
997 contextMenu.appendItem(WebInspector.UIString("Add New"), this._startEditing.bind(this, event.target));
999 contextMenu.appendItem(WebInspector.UIString("Edit"), this._startEditing.bind(this, event.target));
1001 if (this.dataGrid._deleteCallback && gridNode !== this.creationNode)
1002 contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode));
1005 contextMenu.show(event);
1008 _clickInDataTable: function(event)
1010 var gridNode = this.dataGridNodeFromNode(event.target);
1011 if (!gridNode || !gridNode.hasChildren)
1014 if (!gridNode.isEventWithinDisclosureTriangle(event))
1017 if (gridNode.expanded) {
1019 gridNode.collapseRecursively();
1021 gridNode.collapse();
1024 gridNode.expandRecursively();
1032 if (typeof this._resizeMethod === "undefined")
1033 return WebInspector.DataGrid.ResizeMethod.Nearest;
1034 return this._resizeMethod;
1037 set resizeMethod(method)
1039 this._resizeMethod = method;
1042 _startResizerDragging: function(event)
1044 this._currentResizer = event.target;
1045 if (!this._currentResizer.rightNeighboringColumnID)
1047 WebInspector.elementDragStart(this._currentResizer, this._resizerDragging.bind(this),
1048 this._endResizerDragging.bind(this), event, "col-resize");
1051 _resizerDragging: function(event)
1053 var resizer = this._currentResizer;
1057 // Constrain the dragpoint to be within the containing div of the
1059 var dragPoint = event.clientX - this.element.totalOffsetLeft();
1060 // Constrain the dragpoint to be within the space made up by the
1061 // column directly to the left and the column directly to the right.
1062 var leftCellIndex = resizer.leftNeighboringColumnID;
1063 var rightCellIndex = resizer.rightNeighboringColumnID;
1064 var firstRowCells = this.headerTableBody.rows[0].cells;
1065 var leftEdgeOfPreviousColumn = 0;
1066 for (var i = 0; i < leftCellIndex; i++)
1067 leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth;
1069 // Differences for other resize methods
1070 if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.Last) {
1071 rightCellIndex = this.resizers.length;
1072 } else if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.First) {
1073 leftEdgeOfPreviousColumn += firstRowCells[leftCellIndex].offsetWidth - firstRowCells[0].offsetWidth;
1077 var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[leftCellIndex].offsetWidth + firstRowCells[rightCellIndex].offsetWidth;
1079 // Give each column some padding so that they don't disappear.
1080 var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding;
1081 var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding;
1083 dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum);
1085 resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px";
1087 var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / this._dataTable.offsetWidth) * 100) + "%";
1088 this._headerTableColumnGroup.children[leftCellIndex].style.width = percentLeftColumn;
1089 this._dataTableColumnGroup.children[leftCellIndex].style.width = percentLeftColumn;
1091 var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / this._dataTable.offsetWidth) * 100) + "%";
1092 this._headerTableColumnGroup.children[rightCellIndex].style.width = percentRightColumn;
1093 this._dataTableColumnGroup.children[rightCellIndex].style.width = percentRightColumn;
1095 this._positionResizers();
1096 event.preventDefault();
1097 this.dispatchEventToListeners("width changed");
1100 _endResizerDragging: function(event)
1102 WebInspector.elementDragEnd(event);
1103 this._currentResizer = null;
1104 this.dispatchEventToListeners("width changed");
1107 ColumnResizePadding: 10,
1109 CenterResizerOverBorderAdjustment: 3,
1112 WebInspector.DataGrid.ResizeMethod = {
1118 WebInspector.DataGrid.prototype.__proto__ = WebInspector.View.prototype;
1122 * @extends {WebInspector.Object}
1123 * @param {boolean=} hasChildren
1125 WebInspector.DataGridNode = function(data, hasChildren)
1127 this._expanded = false;
1128 this._selected = false;
1129 this._shouldRefreshChildren = true;
1130 this._data = data || {};
1131 this.hasChildren = hasChildren || false;
1133 this.dataGrid = null;
1135 this.previousSibling = null;
1136 this.nextSibling = null;
1137 this.disclosureToggleWidth = 10;
1140 WebInspector.DataGridNode.prototype = {
1146 return this._element;
1151 this._element = document.createElement("tr");
1152 this._element._dataGridNode = this;
1154 if (this.hasChildren)
1155 this._element.addStyleClass("parent");
1157 this._element.addStyleClass("expanded");
1159 this._element.addStyleClass("selected");
1161 this._element.addStyleClass("revealed");
1164 return this._element;
1167 createCells: function()
1169 for (var columnIdentifier in this.dataGrid.columns) {
1170 var cell = this.createCell(columnIdentifier);
1171 this._element.appendChild(cell);
1182 this._data = x || {};
1188 if ("_revealed" in this)
1189 return this._revealed;
1191 var currentAncestor = this.parent;
1192 while (currentAncestor && !currentAncestor.root) {
1193 if (!currentAncestor.expanded) {
1194 this._revealed = false;
1198 currentAncestor = currentAncestor.parent;
1201 this._revealed = true;
1207 if (this._hasChildren === x)
1210 this._hasChildren = x;
1215 if (this._hasChildren)
1217 this._element.addStyleClass("parent");
1219 this._element.addStyleClass("expanded");
1223 this._element.removeStyleClass("parent");
1224 this._element.removeStyleClass("expanded");
1230 return this._hasChildren;
1235 if (this._revealed === x)
1240 if (this._element) {
1242 this._element.addStyleClass("revealed");
1244 this._element.removeStyleClass("revealed");
1247 for (var i = 0; i < this.children.length; ++i)
1248 this.children[i].revealed = x && this.expanded;
1253 if ("_depth" in this)
1255 if (this.parent && !this.parent.root)
1256 this._depth = this.parent.depth + 1;
1264 if (typeof(this._leftPadding) === "number")
1265 return this._leftPadding;
1267 this._leftPadding = this.depth * this.dataGrid.indentWidth;
1268 return this._leftPadding;
1271 get shouldRefreshChildren()
1273 return this._shouldRefreshChildren;
1276 set shouldRefreshChildren(x)
1278 this._shouldRefreshChildren = x;
1279 if (x && this.expanded)
1285 return this._selected;
1298 return this._expanded;
1311 if (!this._element || !this.dataGrid)
1314 this._element.removeChildren();
1318 createCell: function(columnIdentifier)
1320 var cell = document.createElement("td");
1321 cell.className = columnIdentifier + "-column";
1323 var alignment = this.dataGrid.aligned[columnIdentifier];
1325 cell.addStyleClass(alignment);
1327 var div = document.createElement("div");
1328 div.textContent = this.data[columnIdentifier];
1329 cell.appendChild(div);
1331 if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
1332 cell.addStyleClass("disclosure");
1333 if (this.leftPadding)
1334 cell.style.setProperty("padding-left", this.leftPadding + "px");
1340 // Share these functions with DataGrid. They are written to work with a DataGridNode this object.
1341 appendChild: WebInspector.DataGrid.prototype.appendChild,
1342 insertChild: WebInspector.DataGrid.prototype.insertChild,
1343 removeChild: WebInspector.DataGrid.prototype.removeChild,
1344 removeChildren: WebInspector.DataGrid.prototype.removeChildren,
1345 removeChildrenRecursive: WebInspector.DataGrid.prototype.removeChildrenRecursive,
1347 _recalculateSiblings: function(myIndex)
1352 var previousChild = (myIndex > 0 ? this.parent.children[myIndex - 1] : null);
1354 if (previousChild) {
1355 previousChild.nextSibling = this;
1356 this.previousSibling = previousChild;
1358 this.previousSibling = null;
1360 var nextChild = this.parent.children[myIndex + 1];
1363 nextChild.previousSibling = this;
1364 this.nextSibling = nextChild;
1366 this.nextSibling = null;
1369 collapse: function()
1372 this._element.removeStyleClass("expanded");
1374 this._expanded = false;
1376 for (var i = 0; i < this.children.length; ++i)
1377 this.children[i].revealed = false;
1379 this.dispatchEventToListeners("collapsed");
1382 collapseRecursively: function()
1388 item = item.traverseNextNode(false, this, true);
1394 if (!this.hasChildren || this.expanded)
1397 if (this.revealed && !this._shouldRefreshChildren)
1398 for (var i = 0; i < this.children.length; ++i)
1399 this.children[i].revealed = true;
1401 if (this._shouldRefreshChildren) {
1402 for (var i = 0; i < this.children.length; ++i)
1403 this.children[i]._detach();
1405 this.dispatchEventToListeners("populate");
1407 if (this._attached) {
1408 for (var i = 0; i < this.children.length; ++i) {
1409 var child = this.children[i];
1411 child.revealed = true;
1416 delete this._shouldRefreshChildren;
1420 this._element.addStyleClass("expanded");
1422 this._expanded = true;
1424 this.dispatchEventToListeners("expanded");
1427 expandRecursively: function()
1432 item = item.traverseNextNode(false, this);
1438 var currentAncestor = this.parent;
1439 while (currentAncestor && !currentAncestor.root) {
1440 if (!currentAncestor.expanded)
1441 currentAncestor.expand();
1442 currentAncestor = currentAncestor.parent;
1445 this.element.scrollIntoViewIfNeeded(false);
1447 this.dispatchEventToListeners("revealed");
1451 * @param {boolean=} supressSelectedEvent
1453 select: function(supressSelectedEvent)
1455 if (!this.dataGrid || !this.selectable || this.selected)
1458 if (this.dataGrid.selectedNode)
1459 this.dataGrid.selectedNode.deselect();
1461 this._selected = true;
1462 this.dataGrid.selectedNode = this;
1465 this._element.addStyleClass("selected");
1467 if (!supressSelectedEvent)
1468 this.dispatchEventToListeners("selected");
1471 revealAndSelect: function()
1478 * @param {boolean=} supressDeselectedEvent
1480 deselect: function(supressDeselectedEvent)
1482 if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
1485 this._selected = false;
1486 this.dataGrid.selectedNode = null;
1489 this._element.removeStyleClass("selected");
1491 if (!supressDeselectedEvent)
1492 this.dispatchEventToListeners("deselected");
1495 traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info)
1497 if (!dontPopulate && this.hasChildren)
1498 this.dispatchEventToListeners("populate");
1501 info.depthChange = 0;
1503 var node = (!skipHidden || this.revealed) ? this.children[0] : null;
1504 if (node && (!skipHidden || this.expanded)) {
1506 info.depthChange = 1;
1510 if (this === stayWithin)
1513 node = (!skipHidden || this.revealed) ? this.nextSibling : null;
1518 while (node && !node.root && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) {
1520 info.depthChange -= 1;
1527 return (!skipHidden || node.revealed) ? node.nextSibling : null;
1530 traversePreviousNode: function(skipHidden, dontPopulate)
1532 var node = (!skipHidden || this.revealed) ? this.previousSibling : null;
1533 if (!dontPopulate && node && node.hasChildren)
1534 node.dispatchEventToListeners("populate");
1536 while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null)) {
1537 if (!dontPopulate && node.hasChildren)
1538 node.dispatchEventToListeners("populate");
1539 node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null);
1545 if (!this.parent || this.parent.root)
1551 isEventWithinDisclosureTriangle: function(event)
1553 if (!this.hasChildren)
1555 var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
1556 if (!cell.hasStyleClass("disclosure"))
1559 var left = cell.totalOffsetLeft() + this.leftPadding;
1560 return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth;
1565 if (!this.dataGrid || this._attached)
1568 this._attached = true;
1570 var nextNode = null;
1571 var previousNode = this.traversePreviousNode(true, true);
1572 if (previousNode && previousNode.element.parentNode && previousNode.element.nextSibling)
1573 nextNode = previousNode.element.nextSibling;
1575 nextNode = this.dataGrid.dataTableBody.lastChild;
1576 this.dataGrid.dataTableBody.insertBefore(this.element, nextNode);
1579 for (var i = 0; i < this.children.length; ++i)
1580 this.children[i]._attach();
1585 if (!this._attached)
1588 this._attached = false;
1590 if (this._element && this._element.parentNode)
1591 this._element.parentNode.removeChild(this._element);
1593 for (var i = 0; i < this.children.length; ++i)
1594 this.children[i]._detach();
1597 savePosition: function()
1599 if (this._savedPosition)
1603 throw("savePosition: Node must have a parent.");
1604 this._savedPosition = {
1605 parent: this.parent,
1606 index: this.parent.children.indexOf(this)
1610 restorePosition: function()
1612 if (!this._savedPosition)
1615 if (this.parent !== this._savedPosition.parent)
1616 this._savedPosition.parent.insertChild(this, this._savedPosition.index);
1618 delete this._savedPosition;
1622 WebInspector.DataGridNode.prototype.__proto__ = WebInspector.Object.prototype;
1626 * @extends {WebInspector.DataGridNode}
1628 WebInspector.CreationDataGridNode = function(data, hasChildren)
1630 WebInspector.DataGridNode.call(this, data, hasChildren);
1631 this.isCreationNode = true;
1634 WebInspector.CreationDataGridNode.prototype = {
1635 makeNormal: function()
1637 delete this.isCreationNode;
1638 delete this.makeNormal;
1642 WebInspector.CreationDataGridNode.prototype.__proto__ = WebInspector.DataGridNode.prototype;