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, string, string, string)=} editCallback
30 * @param {?function(WebInspector.DataGridNode)=} deleteCallback
31 * @param {?function()=} refreshCallback
33 WebInspector.DataGrid = function(columns, editCallback, deleteCallback, refreshCallback)
35 WebInspector.View.call(this);
36 this.registerRequiredCSS("dataGrid.css");
38 this.element.className = "data-grid";
39 this.element.tabIndex = 0;
40 this.element.addEventListener("keydown", this._keyDown.bind(this), false);
42 this._headerTable = document.createElement("table");
43 this._headerTable.className = "header";
44 this._headerTableHeaders = {};
46 this._dataTable = document.createElement("table");
47 this._dataTable.className = "data";
49 this._dataTable.addEventListener("mousedown", this._mouseDownInDataTable.bind(this), true);
50 this._dataTable.addEventListener("click", this._clickInDataTable.bind(this), true);
52 this._dataTable.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true);
54 // FIXME: Add a createCallback which is different from editCallback and has different
55 // behavior when creating a new node.
57 this._dataTable.addEventListener("dblclick", this._ondblclick.bind(this), false);
58 this._editCallback = editCallback;
59 this._deleteCallback = deleteCallback;
60 this._refreshCallback = refreshCallback;
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 headerRow.createChild("th", "corner");
120 this._headerTableColumnGroup = columnGroup;
121 this._headerTable.appendChild(this._headerTableColumnGroup);
122 this.headerTableBody.appendChild(headerRow);
124 var fillerRow = document.createElement("tr");
125 fillerRow.className = "filler";
127 for (var columnIdentifier in columns)
128 fillerRow.createChild("td", columnIdentifier + "-column");
129 fillerRow.createChild("td", "corner");
131 this._dataTableColumnGroup = columnGroup.cloneNode(true);
132 this._dataTable.appendChild(this._dataTableColumnGroup);
133 this.dataTableBody.appendChild(fillerRow);
135 this.columns = columns || {};
136 this._columnsArray = [];
137 for (var columnIdentifier in columns) {
138 columns[columnIdentifier].ordinal = this._columnsArray.length;
139 columns[columnIdentifier].identifier = columnIdentifier;
140 this._columnsArray.push(columns[columnIdentifier]);
143 for (var i = 0; i < this._columnsArray.length; ++i)
144 this._columnsArray[i].bodyElement = this._dataTableColumnGroup.children[i];
146 this.selectedNode = null;
147 this.expandNodesWhenArrowing = false;
148 this.setRootNode(new WebInspector.DataGridNode());
149 this.indentWidth = 15;
151 this._columnWidthsInitialized = false;
154 WebInspector.DataGrid.Events = {
155 SelectedNode: "SelectedNode",
156 DeselectedNode: "DeselectedNode"
160 * @param {Array.<string>} columnNames
161 * @param {Array.<string>} values
163 WebInspector.DataGrid.createSortableDataGrid = function(columnNames, values)
165 var numColumns = columnNames.length;
171 for (var i = 0; i < columnNames.length; ++i) {
173 column.width = columnNames[i].length;
174 column.title = columnNames[i];
175 column.sortable = true;
177 columns[columnNames[i]] = column;
181 for (var i = 0; i < values.length / numColumns; ++i) {
183 for (var j = 0; j < columnNames.length; ++j)
184 data[columnNames[j]] = values[numColumns * i + j];
186 var node = new WebInspector.DataGridNode(data, false);
187 node.selectable = false;
191 var dataGrid = new WebInspector.DataGrid(columns);
192 var length = nodes.length;
193 for (var i = 0; i < length; ++i)
194 dataGrid.rootNode().appendChild(nodes[i]);
196 dataGrid.addEventListener("sorting changed", sortDataGrid, this);
198 function sortDataGrid()
200 var nodes = dataGrid._rootNode.children.slice();
201 var sortColumnIdentifier = dataGrid.sortColumnIdentifier;
202 var sortDirection = dataGrid.sortOrder === "ascending" ? 1 : -1;
203 var columnIsNumeric = true;
205 for (var i = 0; i < nodes.length; i++) {
206 if (isNaN(Number(nodes[i].data[sortColumnIdentifier])))
207 columnIsNumeric = false;
210 function comparator(dataGridNode1, dataGridNode2)
212 var item1 = dataGridNode1.data[sortColumnIdentifier];
213 var item2 = dataGridNode2.data[sortColumnIdentifier];
216 if (columnIsNumeric) {
217 // Sort numbers based on comparing their values rather than a lexicographical comparison.
218 var number1 = parseFloat(item1);
219 var number2 = parseFloat(item2);
220 comparison = number1 < number2 ? -1 : (number1 > number2 ? 1 : 0);
222 comparison = item1 < item2 ? -1 : (item1 > item2 ? 1 : 0);
224 return sortDirection * comparison;
227 nodes.sort(comparator);
228 dataGrid.rootNode().removeChildren();
229 for (var i = 0; i < nodes.length; i++)
230 dataGrid._rootNode.appendChild(nodes[i]);
235 WebInspector.DataGrid.prototype = {
237 * @param {!WebInspector.DataGridNode} rootNode
239 setRootNode: function(rootNode)
241 if (this._rootNode) {
242 this._rootNode.removeChildren();
243 this._rootNode.dataGrid = null;
244 this._rootNode._isRoot = false;
246 /** @type {!WebInspector.DataGridNode} */
247 this._rootNode = rootNode;
248 rootNode._isRoot = true;
249 rootNode.hasChildren = false;
250 rootNode._expanded = true;
251 rootNode._revealed = true;
252 rootNode.dataGrid = this;
256 * @return {!WebInspector.DataGridNode}
260 return this._rootNode;
263 _ondblclick: function(event)
265 if (this._editing || this._editingNode)
268 var columnIdentifier = this.columnIdentifierFromNode(event.target);
269 if (!columnIdentifier || !this.columns[columnIdentifier].editable)
271 this._startEditing(event.target);
275 * @param {!WebInspector.DataGridNode} node
276 * @param {number} columnOrdinal
278 _startEditingColumnOfDataGridNode: function(node, columnOrdinal)
280 this._editing = true;
281 /** @type {WebInspector.DataGridNode} */
282 this._editingNode = node;
283 this._editingNode.select();
285 var element = this._editingNode._element.children[columnOrdinal];
286 WebInspector.startEditing(element, this._startEditingConfig(element));
287 window.getSelection().setBaseAndExtent(element, 0, element, 1);
290 _startEditing: function(target)
292 var element = target.enclosingNodeOrSelfWithNodeName("td");
296 this._editingNode = this.dataGridNodeFromNode(target);
297 if (!this._editingNode) {
298 if (!this.creationNode)
300 this._editingNode = this.creationNode;
303 // Force editing the 1st column when editing the creation node
304 if (this._editingNode.isCreationNode)
305 return this._startEditingColumnOfDataGridNode(this._editingNode, this._nextEditableColumn(-1));
307 this._editing = true;
308 WebInspector.startEditing(element, this._startEditingConfig(element));
310 window.getSelection().setBaseAndExtent(element, 0, element, 1);
314 _startEditingConfig: function(element)
316 return new WebInspector.EditingConfig(this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent);
319 _editingCommitted: function(element, newText, oldText, context, moveDirection)
321 var columnIdentifier = this.columnIdentifierFromNode(element);
322 if (!columnIdentifier) {
323 this._editingCancelled(element);
326 var columnOrdinal = this.columns[columnIdentifier].ordinal;
327 var textBeforeEditing = this._editingNode.data[columnIdentifier];
328 var currentEditingNode = this._editingNode;
330 function moveToNextIfNeeded(wasChange) {
334 if (moveDirection === "forward") {
335 var firstEditableColumn = this._nextEditableColumn(-1);
336 if (currentEditingNode.isCreationNode && columnOrdinal === firstEditableColumn && !wasChange)
339 var nextEditableColumn = this._nextEditableColumn(columnOrdinal);
340 if (nextEditableColumn !== -1)
341 return this._startEditingColumnOfDataGridNode(currentEditingNode, nextEditableColumn);
343 var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true);
344 if (nextDataGridNode)
345 return this._startEditingColumnOfDataGridNode(nextDataGridNode, firstEditableColumn);
346 if (currentEditingNode.isCreationNode && wasChange) {
347 this.addCreationNode(false);
348 return this._startEditingColumnOfDataGridNode(this.creationNode, firstEditableColumn);
353 if (moveDirection === "backward") {
354 var prevEditableColumn = this._nextEditableColumn(columnOrdinal, true);
355 if (prevEditableColumn !== -1)
356 return this._startEditingColumnOfDataGridNode(currentEditingNode, prevEditableColumn);
358 var lastEditableColumn = this._nextEditableColumn(this._columnsArray.length, true);
359 var nextDataGridNode = currentEditingNode.traversePreviousNode(true, true);
360 if (nextDataGridNode)
361 return this._startEditingColumnOfDataGridNode(nextDataGridNode, lastEditableColumn);
366 if (textBeforeEditing == newText) {
367 this._editingCancelled(element);
368 moveToNextIfNeeded.call(this, false);
372 // Update the text in the datagrid that we typed
373 this._editingNode.data[columnIdentifier] = newText;
375 // Make the callback - expects an editing node (table row), the column number that is being edited,
376 // the text that used to be there, and the new text.
377 this._editCallback(this._editingNode, columnIdentifier, textBeforeEditing, newText);
379 if (this._editingNode.isCreationNode)
380 this.addCreationNode(false);
382 this._editingCancelled(element);
383 moveToNextIfNeeded.call(this, true);
386 _editingCancelled: function(element)
388 delete this._editing;
389 this._editingNode = null;
393 * @param {number} columnOrdinal
394 * @param {boolean=} moveBackward
397 _nextEditableColumn: function(columnOrdinal, moveBackward)
399 var increment = moveBackward ? -1 : 1;
400 var columns = this._columnsArray;
401 for (var i = columnOrdinal + increment; (i >= 0) && (i < columns.length); i += increment) {
402 if (columns[i].editable)
403 return columns[i].ordinal;
411 get sortColumnIdentifier()
413 if (!this._sortColumnCell)
415 return this._sortColumnCell.columnIdentifier;
423 if (!this._sortColumnCell || this._sortColumnCell.hasStyleClass("sort-ascending"))
425 if (this._sortColumnCell.hasStyleClass("sort-descending"))
430 get headerTableBody()
432 if ("_headerTableBody" in this)
433 return this._headerTableBody;
435 this._headerTableBody = this._headerTable.getElementsByTagName("tbody")[0];
436 if (!this._headerTableBody) {
437 this._headerTableBody = this.element.ownerDocument.createElement("tbody");
438 this._headerTable.insertBefore(this._headerTableBody, this._headerTable.tFoot);
441 return this._headerTableBody;
446 if ("_dataTableBody" in this)
447 return this._dataTableBody;
449 this._dataTableBody = this._dataTable.getElementsByTagName("tbody")[0];
450 if (!this._dataTableBody) {
451 this._dataTableBody = this.element.ownerDocument.createElement("tbody");
452 this._dataTable.insertBefore(this._dataTableBody, this._dataTable.tFoot);
455 return this._dataTableBody;
459 * @param {Array.<number>} widths
460 * @param {number} minPercent
461 * @param {number=} maxPercent
463 _autoSizeWidths: function(widths, minPercent, maxPercent)
466 minPercent = Math.min(minPercent, Math.floor(100 / widths.length));
468 for (var i = 0; i < widths.length; ++i)
469 totalWidth += widths[i];
470 var totalPercentWidth = 0;
471 for (var i = 0; i < widths.length; ++i) {
472 var width = Math.round(100 * widths[i] / totalWidth);
473 if (minPercent && width < minPercent)
475 else if (maxPercent && width > maxPercent)
477 totalPercentWidth += width;
480 var recoupPercent = totalPercentWidth - 100;
482 while (minPercent && recoupPercent > 0) {
483 for (var i = 0; i < widths.length; ++i) {
484 if (widths[i] > minPercent) {
493 while (maxPercent && recoupPercent < 0) {
494 for (var i = 0; i < widths.length; ++i) {
495 if (widths[i] < maxPercent) {
508 * @param {number} minPercent
509 * @param {number=} maxPercent
510 * @param {number=} maxDescentLevel
512 autoSizeColumns: function(minPercent, maxPercent, maxDescentLevel)
515 var columnIdentifiers = Object.keys(this.columns);
516 for (var i = 0; i < columnIdentifiers.length; ++i)
517 widths[i] = (this.columns[columnIdentifiers[i]].title || "").length;
519 maxDescentLevel = maxDescentLevel || 0;
520 var children = this._enumerateChildren(this._rootNode, [], maxDescentLevel + 1);
521 for (var i = 0; i < children.length; ++i) {
522 var node = children[i];
523 for (var j = 0; j < columnIdentifiers.length; ++j) {
524 var text = node.data[columnIdentifiers[j]] || "";
525 if (text.length > widths[j])
526 widths[j] = text.length;
530 widths = this._autoSizeWidths(widths, minPercent, maxPercent);
532 for (var i = 0; i < columnIdentifiers.length; ++i)
533 this.columns[columnIdentifiers[i]].element.style.width = widths[i] + "%";
534 this._columnWidthsInitialized = false;
538 _enumerateChildren: function(rootNode, result, maxLevel)
540 if (!rootNode._isRoot)
541 result.push(rootNode);
544 for (var i = 0; i < rootNode.children.length; ++i)
545 this._enumerateChildren(rootNode.children[i], result, maxLevel - 1);
554 // Updates the widths of the table, including the positions of the column
557 // IMPORTANT: This function MUST be called once after the element of the
558 // DataGrid is attached to its parent element and every subsequent time the
559 // width of the parent element is changed in order to make it possible to
560 // resize the columns.
562 // If this function is not called after the DataGrid is attached to its
563 // parent element, then the DataGrid's columns will not be resizable.
564 updateWidths: function()
566 var headerTableColumns = this._headerTableColumnGroup.children;
568 var tableWidth = this._dataTable.offsetWidth;
569 var numColumns = headerTableColumns.length;
571 // Do not attempt to use offsetes if we're not attached to the document tree yet.
572 if (!this._columnWidthsInitialized && this.element.offsetWidth) {
573 // Give all the columns initial widths now so that during a resize,
574 // when the two columns that get resized get a percent value for
575 // their widths, all the other columns already have percent values
577 for (var i = 0; i < numColumns; i++) {
578 var columnWidth = this.headerTableBody.rows[0].cells[i].offsetWidth;
579 var percentWidth = ((columnWidth / tableWidth) * 100) + "%";
580 this._headerTableColumnGroup.children[i].style.width = percentWidth;
581 this._dataTableColumnGroup.children[i].style.width = percentWidth;
583 this._columnWidthsInitialized = true;
585 this._positionResizers();
586 this.dispatchEventToListeners("width changed");
589 applyColumnWeights: function()
591 var sumOfWeights = 0.0;
592 for (var columnIdentifier in this.columns) {
593 if (this.isColumnVisible(columnIdentifier))
594 sumOfWeights += this.columns[columnIdentifier].weight;
596 var factor = 100 / sumOfWeights;
598 for (var columnIdentifier in this.columns) {
599 var column = this.columns[columnIdentifier];
600 var width = this.isColumnVisible(columnIdentifier) ? ((factor * column.weight) + "%"): "0%";
601 this._headerTableColumnGroup.children[column.ordinal].style.width = width;
602 this._dataTableColumnGroup.children[column.ordinal].style.width = width;
605 this._positionResizers();
606 this.dispatchEventToListeners("width changed");
610 * @param {string} columnIdentifier
612 isColumnVisible: function(columnIdentifier)
614 return !this.columns[columnIdentifier].hidden;
618 * @param {string} columnIdentifier
619 * @param {boolean} visible
621 setColumnVisible: function(columnIdentifier, visible)
623 if (visible === !this.columns[columnIdentifier].hidden)
626 this.columns[columnIdentifier].hidden = !visible;
628 this.element.removeStyleClass("hide-" + columnIdentifier + "-column");
630 this.element.addStyleClass("hide-" + columnIdentifier + "-column");
633 get scrollContainer()
635 return this._scrollContainer;
638 isScrolledToLastRow: function()
640 return this._scrollContainer.isScrolledToBottom();
643 scrollToLastRow: function()
645 this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight - this._scrollContainer.offsetHeight;
648 _positionResizers: function()
650 var headerTableColumns = this._headerTableColumnGroup.children;
651 var numColumns = headerTableColumns.length;
653 var previousResizer = null;
655 // Make n - 1 resizers for n columns.
656 for (var i = 0; i < numColumns - 1; i++) {
657 var resizer = this.resizers[i];
660 // This is the first call to updateWidth, so the resizers need
662 resizer = document.createElement("div");
663 resizer.addStyleClass("data-grid-resizer");
664 // This resizer is associated with the column to its right.
665 WebInspector.installDragHandle(resizer, this._startResizerDragging.bind(this), this._resizerDragging.bind(this), this._endResizerDragging.bind(this), "col-resize");
666 this.element.appendChild(resizer);
667 this.resizers[i] = resizer;
670 // Get the width of the cell in the first (and only) row of the
671 // header table in order to determine the width of the column, since
672 // it is not possible to query a column for its width.
673 left += this.headerTableBody.rows[0].cells[i].offsetWidth;
675 if (!this._columnsArray[i].hidden) {
676 resizer.style.removeProperty("display");
677 if (resizer._position !== left) {
678 resizer._position = left;
679 resizer.style.left = left + "px";
681 resizer.leftNeighboringColumnIndex = i;
683 previousResizer.rightNeighboringColumnIndex = i;
684 previousResizer = resizer;
686 resizer.style.setProperty("display", "none");
687 resizer.leftNeighboringColumnIndex = 0;
688 resizer.rightNeighboringColumnIndex = 0;
692 previousResizer.rightNeighboringColumnIndex = numColumns - 1;
695 addCreationNode: function(hasChildren)
697 if (this.creationNode)
698 this.creationNode.makeNormal();
701 for (var column in this.columns)
702 emptyData[column] = '';
703 this.creationNode = new WebInspector.CreationDataGridNode(emptyData, hasChildren);
704 this.rootNode().appendChild(this.creationNode);
707 sortNodes: function(comparator, reverseMode)
709 function comparatorWrapper(a, b)
711 if (a._dataGridNode._data.summaryRow)
713 if (b._dataGridNode._data.summaryRow)
716 var aDataGirdNode = a._dataGridNode;
717 var bDataGirdNode = b._dataGridNode;
718 return reverseMode ? comparator(bDataGirdNode, aDataGirdNode) : comparator(aDataGirdNode, bDataGirdNode);
721 var tbody = this.dataTableBody;
722 var tbodyParent = tbody.parentElement;
723 tbodyParent.removeChild(tbody);
725 var childNodes = tbody.childNodes;
726 var fillerRow = childNodes[childNodes.length - 1];
728 var sortedRows = Array.prototype.slice.call(childNodes, 0, childNodes.length - 1);
729 sortedRows.sort(comparatorWrapper);
730 var sortedRowsLength = sortedRows.length;
732 tbody.removeChildren();
733 var previousSiblingNode = null;
734 for (var i = 0; i < sortedRowsLength; ++i) {
735 var row = sortedRows[i];
736 var node = row._dataGridNode;
737 node.previousSibling = previousSiblingNode;
738 if (previousSiblingNode)
739 previousSiblingNode.nextSibling = node;
740 tbody.appendChild(row);
741 previousSiblingNode = node;
743 if (previousSiblingNode)
744 previousSiblingNode.nextSibling = null;
746 tbody.appendChild(fillerRow);
747 tbodyParent.appendChild(tbody);
750 _keyDown: function(event)
752 if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing)
756 var nextSelectedNode;
757 if (event.keyIdentifier === "Up" && !event.altKey) {
758 nextSelectedNode = this.selectedNode.traversePreviousNode(true);
759 while (nextSelectedNode && !nextSelectedNode.selectable)
760 nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
761 handled = nextSelectedNode ? true : false;
762 } else if (event.keyIdentifier === "Down" && !event.altKey) {
763 nextSelectedNode = this.selectedNode.traverseNextNode(true);
764 while (nextSelectedNode && !nextSelectedNode.selectable)
765 nextSelectedNode = nextSelectedNode.traverseNextNode(true);
766 handled = nextSelectedNode ? true : false;
767 } else if (event.keyIdentifier === "Left") {
768 if (this.selectedNode.expanded) {
770 this.selectedNode.collapseRecursively();
772 this.selectedNode.collapse();
774 } else if (this.selectedNode.parent && !this.selectedNode.parent._isRoot) {
776 if (this.selectedNode.parent.selectable) {
777 nextSelectedNode = this.selectedNode.parent;
778 handled = nextSelectedNode ? true : false;
779 } else if (this.selectedNode.parent)
780 this.selectedNode.parent.collapse();
782 } else if (event.keyIdentifier === "Right") {
783 if (!this.selectedNode.revealed) {
784 this.selectedNode.reveal();
786 } else if (this.selectedNode.hasChildren) {
788 if (this.selectedNode.expanded) {
789 nextSelectedNode = this.selectedNode.children[0];
790 handled = nextSelectedNode ? true : false;
793 this.selectedNode.expandRecursively();
795 this.selectedNode.expand();
798 } else if (event.keyCode === 8 || event.keyCode === 46) {
799 if (this._deleteCallback) {
801 this._deleteCallback(this.selectedNode);
803 } else if (isEnterKey(event)) {
804 if (this._editCallback) {
806 this._startEditing(this.selectedNode._element.children[this._nextEditableColumn(-1)]);
810 if (nextSelectedNode) {
811 nextSelectedNode.reveal();
812 nextSelectedNode.select();
820 * @param {!Node} target
821 * @return {?WebInspector.DataGridNode}
823 dataGridNodeFromNode: function(target)
825 var rowElement = target.enclosingNodeOrSelfWithNodeName("tr");
826 return rowElement && rowElement._dataGridNode;
830 * @param {!Node} target
833 columnIdentifierFromNode: function(target)
835 var cellElement = target.enclosingNodeOrSelfWithNodeName("td");
836 return cellElement && cellElement.columnIdentifier_;
839 _clickInHeaderCell: function(event)
841 var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
842 if (!cell || !cell.columnIdentifier || !cell.hasStyleClass("sortable"))
845 var sortOrder = this.sortOrder;
847 if (this._sortColumnCell)
848 this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
850 if (cell == this._sortColumnCell) {
851 if (sortOrder === "ascending")
852 sortOrder = "descending";
854 sortOrder = "ascending";
857 this._sortColumnCell = cell;
859 cell.addStyleClass("sort-" + sortOrder);
861 this.dispatchEventToListeners("sorting changed");
864 markColumnAsSortedBy: function(columnIdentifier, sortOrder)
866 if (this._sortColumnCell)
867 this._sortColumnCell.removeMatchingStyleClasses("sort-\\w+");
868 this._sortColumnCell = this._headerTableHeaders[columnIdentifier];
869 this._sortColumnCell.addStyleClass("sort-" + sortOrder);
872 headerTableHeader: function(columnIdentifier)
874 return this._headerTableHeaders[columnIdentifier];
877 _mouseDownInDataTable: function(event)
879 var gridNode = this.dataGridNodeFromNode(event.target);
880 if (!gridNode || !gridNode.selectable)
883 if (gridNode.isEventWithinDisclosureTriangle(event))
887 if (gridNode.selected)
895 _contextMenuInDataTable: function(event)
897 var contextMenu = new WebInspector.ContextMenu(event);
899 var gridNode = this.dataGridNodeFromNode(event.target);
900 if (this._refreshCallback && (!gridNode || gridNode !== this.creationNode))
901 contextMenu.appendItem(WebInspector.UIString("Refresh"), this._refreshCallback.bind(this));
903 if (gridNode && gridNode.selectable && !gridNode.isEventWithinDisclosureTriangle(event)) {
904 // FIXME: Use the column names for Editing, instead of just "Edit".
905 if (this._editCallback) {
906 if (gridNode === this.creationNode)
907 contextMenu.appendItem(WebInspector.UIString("Add New"), this._startEditing.bind(this, event.target));
909 var columnIdentifier = this.columnIdentifierFromNode(event.target);
910 if (columnIdentifier && this.columns[columnIdentifier].editable)
911 contextMenu.appendItem(WebInspector.UIString("Edit"), this._startEditing.bind(this, event.target));
914 if (this._deleteCallback && gridNode !== this.creationNode)
915 contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode));
921 _clickInDataTable: function(event)
923 var gridNode = this.dataGridNodeFromNode(event.target);
924 if (!gridNode || !gridNode.hasChildren)
927 if (!gridNode.isEventWithinDisclosureTriangle(event))
930 if (gridNode.expanded) {
932 gridNode.collapseRecursively();
937 gridNode.expandRecursively();
945 if (typeof this._resizeMethod === "undefined")
946 return WebInspector.DataGrid.ResizeMethod.Nearest;
947 return this._resizeMethod;
950 set resizeMethod(method)
952 this._resizeMethod = method;
958 _startResizerDragging: function(event)
960 this._currentResizer = event.target;
961 return !!this._currentResizer.rightNeighboringColumnIndex
964 _resizerDragging: function(event)
966 var resizer = this._currentResizer;
970 var tableWidth = this._dataTable.offsetWidth; // Cache it early, before we invalidate layout.
972 // Constrain the dragpoint to be within the containing div of the
974 var dragPoint = event.clientX - this.element.totalOffsetLeft();
975 // Constrain the dragpoint to be within the space made up by the
976 // column directly to the left and the column directly to the right.
977 var leftCellIndex = resizer.leftNeighboringColumnIndex;
978 var rightCellIndex = resizer.rightNeighboringColumnIndex;
979 var firstRowCells = this.headerTableBody.rows[0].cells;
980 var leftEdgeOfPreviousColumn = 0;
981 for (var i = 0; i < leftCellIndex; i++)
982 leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth;
984 // Differences for other resize methods
985 if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.Last) {
986 rightCellIndex = this.resizers.length;
987 } else if (this.resizeMethod == WebInspector.DataGrid.ResizeMethod.First) {
988 leftEdgeOfPreviousColumn += firstRowCells[leftCellIndex].offsetWidth - firstRowCells[0].offsetWidth;
992 var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[leftCellIndex].offsetWidth + firstRowCells[rightCellIndex].offsetWidth;
994 // Give each column some padding so that they don't disappear.
995 var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding;
996 var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding;
998 dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum);
1000 resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px";
1002 var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / tableWidth) * 100) + "%";
1003 this._headerTableColumnGroup.children[leftCellIndex].style.width = percentLeftColumn;
1004 this._dataTableColumnGroup.children[leftCellIndex].style.width = percentLeftColumn;
1006 var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / tableWidth) * 100) + "%";
1007 this._headerTableColumnGroup.children[rightCellIndex].style.width = percentRightColumn;
1008 this._dataTableColumnGroup.children[rightCellIndex].style.width = percentRightColumn;
1010 var leftColumn = this._columnsArray[leftCellIndex];
1011 var rightColumn = this._columnsArray[rightCellIndex];
1012 if (leftColumn.weight || rightColumn.weight) {
1013 var sumOfWeights = leftColumn.weight + rightColumn.weight;
1014 var delta = rightEdgeOfNextColumn - leftEdgeOfPreviousColumn;
1015 leftColumn.weight = (dragPoint - leftEdgeOfPreviousColumn) * sumOfWeights / delta;
1016 rightColumn.weight = (rightEdgeOfNextColumn - dragPoint) * sumOfWeights / delta;
1019 this._positionResizers();
1020 event.preventDefault();
1021 this.dispatchEventToListeners("width changed");
1024 _endResizerDragging: function(event)
1026 this._currentResizer = null;
1027 this.dispatchEventToListeners("width changed");
1030 ColumnResizePadding: 10,
1032 CenterResizerOverBorderAdjustment: 3,
1034 __proto__: WebInspector.View.prototype
1037 WebInspector.DataGrid.ResizeMethod = {
1045 * @extends {WebInspector.Object}
1047 * @param {boolean=} hasChildren
1049 WebInspector.DataGridNode = function(data, hasChildren)
1051 this._expanded = false;
1052 this._selected = false;
1053 this._shouldRefreshChildren = true;
1054 this._data = data || {};
1055 this.hasChildren = hasChildren || false;
1056 /** @type {!Array.<WebInspector.DataGridNode>} */
1058 this.dataGrid = null;
1060 /** @type {WebInspector.DataGridNode} */
1061 this.previousSibling = null;
1062 /** @type {WebInspector.DataGridNode} */
1063 this.nextSibling = null;
1064 this.disclosureToggleWidth = 10;
1067 WebInspector.DataGridNode.prototype = {
1068 /** @type {boolean} */
1071 /** @type {boolean} */
1077 return this._element;
1082 this._element = document.createElement("tr");
1083 this._element._dataGridNode = this;
1085 if (this.hasChildren)
1086 this._element.addStyleClass("parent");
1088 this._element.addStyleClass("expanded");
1090 this._element.addStyleClass("selected");
1092 this._element.addStyleClass("revealed");
1095 this._element.createChild("td", "corner");
1097 return this._element;
1100 createCells: function()
1102 for (var columnIdentifier in this.dataGrid.columns) {
1103 var cell = this.createCell(columnIdentifier);
1104 this._element.appendChild(cell);
1115 this._data = x || {};
1121 if ("_revealed" in this)
1122 return this._revealed;
1124 var currentAncestor = this.parent;
1125 while (currentAncestor && !currentAncestor._isRoot) {
1126 if (!currentAncestor.expanded) {
1127 this._revealed = false;
1131 currentAncestor = currentAncestor.parent;
1134 this._revealed = true;
1140 if (this._hasChildren === x)
1143 this._hasChildren = x;
1148 if (this._hasChildren)
1150 this._element.addStyleClass("parent");
1152 this._element.addStyleClass("expanded");
1156 this._element.removeStyleClass("parent");
1157 this._element.removeStyleClass("expanded");
1163 return this._hasChildren;
1168 if (this._revealed === x)
1173 if (this._element) {
1175 this._element.addStyleClass("revealed");
1177 this._element.removeStyleClass("revealed");
1180 for (var i = 0; i < this.children.length; ++i)
1181 this.children[i].revealed = x && this.expanded;
1186 if ("_depth" in this)
1188 if (this.parent && !this.parent._isRoot)
1189 this._depth = this.parent.depth + 1;
1197 if (typeof(this._leftPadding) === "number")
1198 return this._leftPadding;
1200 this._leftPadding = this.depth * this.dataGrid.indentWidth;
1201 return this._leftPadding;
1204 get shouldRefreshChildren()
1206 return this._shouldRefreshChildren;
1209 set shouldRefreshChildren(x)
1211 this._shouldRefreshChildren = x;
1212 if (x && this.expanded)
1218 return this._selected;
1231 return this._expanded;
1244 if (!this._element || !this.dataGrid)
1247 this._element.removeChildren();
1252 * @param {string} columnIdentifier
1253 * @return {!Element}
1255 createTD: function(columnIdentifier)
1257 var cell = document.createElement("td");
1258 cell.className = columnIdentifier + "-column";
1259 cell.columnIdentifier_ = columnIdentifier;
1261 var alignment = this.dataGrid.aligned[columnIdentifier];
1263 cell.addStyleClass(alignment);
1269 * @param {string} columnIdentifier
1270 * @return {!Element}
1272 createCell: function(columnIdentifier)
1274 var cell = this.createTD(columnIdentifier);
1276 var data = this.data[columnIdentifier];
1277 var div = document.createElement("div");
1278 if (data instanceof Node)
1279 div.appendChild(data);
1281 div.textContent = data;
1282 cell.appendChild(div);
1284 if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
1285 cell.addStyleClass("disclosure");
1286 if (this.leftPadding)
1287 cell.style.setProperty("padding-left", this.leftPadding + "px");
1296 nodeHeight: function()
1303 var result = rowHeight;
1304 for (var i = 0; i < this.children.length; i++)
1305 result += this.children[i].nodeHeight();
1310 * @param {WebInspector.DataGridNode} child
1312 appendChild: function(child)
1314 this.insertChild(child, this.children.length);
1318 * @param {WebInspector.DataGridNode} child
1319 * @param {number} index
1321 insertChild: function(child, index)
1324 throw("insertChild: Node can't be undefined or null.");
1325 if (child.parent === this)
1326 throw("insertChild: Node is already a child of this node.");
1329 child.parent.removeChild(child);
1331 this.children.splice(index, 0, child);
1332 this.hasChildren = true;
1334 child.parent = this;
1335 child.dataGrid = this.dataGrid;
1336 child._recalculateSiblings(index);
1338 delete child._depth;
1339 delete child._revealed;
1340 delete child._attached;
1341 child._shouldRefreshChildren = true;
1343 var current = child.children[0];
1345 current.dataGrid = this.dataGrid;
1346 delete current._depth;
1347 delete current._revealed;
1348 delete current._attached;
1349 current._shouldRefreshChildren = true;
1350 current = current.traverseNextNode(false, child, true);
1356 child.revealed = false;
1360 * @param {WebInspector.DataGridNode} child
1362 removeChild: function(child)
1365 throw("removeChild: Node can't be undefined or null.");
1366 if (child.parent !== this)
1367 throw("removeChild: Node is not a child of this node.");
1372 this.children.remove(child, true);
1374 if (child.previousSibling)
1375 child.previousSibling.nextSibling = child.nextSibling;
1376 if (child.nextSibling)
1377 child.nextSibling.previousSibling = child.previousSibling;
1379 child.dataGrid = null;
1380 child.parent = null;
1381 child.nextSibling = null;
1382 child.previousSibling = null;
1384 if (this.children.length <= 0)
1385 this.hasChildren = false;
1388 removeChildren: function()
1390 for (var i = 0; i < this.children.length; ++i) {
1391 var child = this.children[i];
1395 child.dataGrid = null;
1396 child.parent = null;
1397 child.nextSibling = null;
1398 child.previousSibling = null;
1402 this.hasChildren = false;
1405 _recalculateSiblings: function(myIndex)
1410 var previousChild = (myIndex > 0 ? this.parent.children[myIndex - 1] : null);
1412 if (previousChild) {
1413 previousChild.nextSibling = this;
1414 this.previousSibling = previousChild;
1416 this.previousSibling = null;
1418 var nextChild = this.parent.children[myIndex + 1];
1421 nextChild.previousSibling = this;
1422 this.nextSibling = nextChild;
1424 this.nextSibling = null;
1427 collapse: function()
1432 this._element.removeStyleClass("expanded");
1434 this._expanded = false;
1436 for (var i = 0; i < this.children.length; ++i)
1437 this.children[i].revealed = false;
1439 this.dispatchEventToListeners("collapsed");
1442 collapseRecursively: function()
1448 item = item.traverseNextNode(false, this, true);
1454 if (!this.hasChildren || this.expanded)
1459 if (this.revealed && !this._shouldRefreshChildren)
1460 for (var i = 0; i < this.children.length; ++i)
1461 this.children[i].revealed = true;
1463 if (this._shouldRefreshChildren) {
1464 for (var i = 0; i < this.children.length; ++i)
1465 this.children[i]._detach();
1467 this.dispatchEventToListeners("populate");
1469 if (this._attached) {
1470 for (var i = 0; i < this.children.length; ++i) {
1471 var child = this.children[i];
1473 child.revealed = true;
1478 delete this._shouldRefreshChildren;
1482 this._element.addStyleClass("expanded");
1484 this._expanded = true;
1486 this.dispatchEventToListeners("expanded");
1489 expandRecursively: function()
1494 item = item.traverseNextNode(false, this);
1502 var currentAncestor = this.parent;
1503 while (currentAncestor && !currentAncestor._isRoot) {
1504 if (!currentAncestor.expanded)
1505 currentAncestor.expand();
1506 currentAncestor = currentAncestor.parent;
1509 this.element.scrollIntoViewIfNeeded(false);
1511 this.dispatchEventToListeners("revealed");
1515 * @param {boolean=} supressSelectedEvent
1517 select: function(supressSelectedEvent)
1519 if (!this.dataGrid || !this.selectable || this.selected)
1522 if (this.dataGrid.selectedNode)
1523 this.dataGrid.selectedNode.deselect();
1525 this._selected = true;
1526 this.dataGrid.selectedNode = this;
1529 this._element.addStyleClass("selected");
1531 if (!supressSelectedEvent) {
1532 this.dispatchEventToListeners("selected");
1533 this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Events.SelectedNode);
1537 revealAndSelect: function()
1546 * @param {boolean=} supressDeselectedEvent
1548 deselect: function(supressDeselectedEvent)
1550 if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
1553 this._selected = false;
1554 this.dataGrid.selectedNode = null;
1557 this._element.removeStyleClass("selected");
1559 if (!supressDeselectedEvent) {
1560 this.dispatchEventToListeners("deselected");
1561 this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Events.DeselectedNode);
1566 * @param {boolean} skipHidden
1567 * @param {WebInspector.DataGridNode=} stayWithin
1568 * @param {boolean=} dontPopulate
1569 * @param {Object=} info
1570 * @return {WebInspector.DataGridNode}
1572 traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info)
1574 if (!dontPopulate && this.hasChildren)
1575 this.dispatchEventToListeners("populate");
1578 info.depthChange = 0;
1580 var node = (!skipHidden || this.revealed) ? this.children[0] : null;
1581 if (node && (!skipHidden || this.expanded)) {
1583 info.depthChange = 1;
1587 if (this === stayWithin)
1590 node = (!skipHidden || this.revealed) ? this.nextSibling : null;
1595 while (node && !node._isRoot && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) {
1597 info.depthChange -= 1;
1604 return (!skipHidden || node.revealed) ? node.nextSibling : null;
1608 * @param {boolean} skipHidden
1609 * @param {boolean=} dontPopulate
1610 * @return {WebInspector.DataGridNode}
1612 traversePreviousNode: function(skipHidden, dontPopulate)
1614 var node = (!skipHidden || this.revealed) ? this.previousSibling : null;
1615 if (!dontPopulate && node && node.hasChildren)
1616 node.dispatchEventToListeners("populate");
1618 while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null)) {
1619 if (!dontPopulate && node.hasChildren)
1620 node.dispatchEventToListeners("populate");
1621 node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null);
1627 if (!this.parent || this.parent._isRoot)
1633 isEventWithinDisclosureTriangle: function(event)
1635 if (!this.hasChildren)
1637 var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
1638 if (!cell.hasStyleClass("disclosure"))
1641 var left = cell.totalOffsetLeft() + this.leftPadding;
1642 return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth;
1647 if (!this.dataGrid || this._attached)
1650 this._attached = true;
1652 var nextNode = null;
1653 var previousNode = this.traversePreviousNode(true, true);
1654 if (previousNode && previousNode.element.parentNode && previousNode.element.nextSibling)
1655 nextNode = previousNode.element.nextSibling;
1657 nextNode = this.dataGrid.dataTableBody.firstChild;
1658 this.dataGrid.dataTableBody.insertBefore(this.element, nextNode);
1661 for (var i = 0; i < this.children.length; ++i)
1662 this.children[i]._attach();
1667 if (!this._attached)
1670 this._attached = false;
1672 if (this._element && this._element.parentNode)
1673 this._element.parentNode.removeChild(this._element);
1675 for (var i = 0; i < this.children.length; ++i)
1676 this.children[i]._detach();
1681 wasDetached: function()
1685 savePosition: function()
1687 if (this._savedPosition)
1691 throw("savePosition: Node must have a parent.");
1692 this._savedPosition = {
1693 parent: this.parent,
1694 index: this.parent.children.indexOf(this)
1698 restorePosition: function()
1700 if (!this._savedPosition)
1703 if (this.parent !== this._savedPosition.parent)
1704 this._savedPosition.parent.insertChild(this, this._savedPosition.index);
1706 delete this._savedPosition;
1709 __proto__: WebInspector.Object.prototype
1714 * @extends {WebInspector.DataGridNode}
1716 WebInspector.CreationDataGridNode = function(data, hasChildren)
1718 WebInspector.DataGridNode.call(this, data, hasChildren);
1719 this.isCreationNode = true;
1722 WebInspector.CreationDataGridNode.prototype = {
1723 makeNormal: function()
1725 delete this.isCreationNode;
1726 delete this.makeNormal;
1729 __proto__: WebInspector.DataGridNode.prototype