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 {!Array.<!WebInspector.DataGrid.ColumnDescriptor>} columnsArray
30 * @param {function(!WebInspector.DataGridNode, string, string, string)=} editCallback
31 * @param {function(!WebInspector.DataGridNode)=} deleteCallback
32 * @param {function()=} refreshCallback
33 * @param {function(!WebInspector.ContextMenu, !WebInspector.DataGridNode)=} contextMenuCallback
35 WebInspector.DataGrid = function(columnsArray, editCallback, deleteCallback, refreshCallback, contextMenuCallback)
37 WebInspector.View.call(this);
38 this.registerRequiredCSS("dataGrid.css");
40 this.element.className = "data-grid"; // Override
41 this.element.tabIndex = 0;
42 this.element.addEventListener("keydown", this._keyDown.bind(this), false);
44 var headerContainer = document.createElementWithClass("div", "header-container");
45 /** @type {!Element} */
46 this._headerTable = headerContainer.createChild("table", "header");
47 /** @type {!Object.<string, !Element>} */
48 this._headerTableHeaders = {};
50 /** @type {!Element} */
51 this._scrollContainer = document.createElementWithClass("div", "data-container");
52 /** @type {!Element} */
53 this._dataTable = this._scrollContainer.createChild("table", "data");
55 this._dataTable.addEventListener("mousedown", this._mouseDownInDataTable.bind(this), true);
56 this._dataTable.addEventListener("click", this._clickInDataTable.bind(this), true);
58 this._dataTable.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true);
60 // FIXME: Add a createCallback which is different from editCallback and has different
61 // behavior when creating a new node.
63 this._dataTable.addEventListener("dblclick", this._ondblclick.bind(this), false);
64 /** @type {function(!WebInspector.DataGridNode, string, string, string)|undefined} */
65 this._editCallback = editCallback;
66 /** @type {function(!WebInspector.DataGridNode)|undefined} */
67 this._deleteCallback = deleteCallback;
68 /** @type {function()|undefined} */
69 this._refreshCallback = refreshCallback;
70 /** @type {function(!WebInspector.ContextMenu, !WebInspector.DataGridNode)|undefined} */
71 this._contextMenuCallback = contextMenuCallback;
73 this.element.appendChild(headerContainer);
74 this.element.appendChild(this._scrollContainer);
76 /** @type {!Element} */
77 this._headerRow = document.createElement("tr");
78 /** @type {!Element} */
79 this._headerTableColumnGroup = document.createElement("colgroup");
80 /** @type {!Element} */
81 this._dataTableColumnGroup = document.createElement("colgroup");
83 /** @type {!Element} */
84 this._topFillerRow = document.createElementWithClass("tr", "revealed");
85 /** @type {!Element} */
86 this._bottomFillerRow = document.createElementWithClass("tr", "revealed");
87 this.setVerticalPadding(0, 0);
89 /** @type {!Array.<!WebInspector.DataGrid.ColumnDescriptor>} */
90 this._columnsArray = columnsArray;
91 /** @type {!Array.<!WebInspector.DataGrid.ColumnDescriptor>} */
92 this._visibleColumnsArray = columnsArray;
93 /** @type {!Object.<string, !WebInspector.DataGrid.ColumnDescriptor>} */
96 /** @type {?string} */
97 this._cellClass = null;
99 for (var i = 0; i < columnsArray.length; ++i) {
100 var column = columnsArray[i];
101 var columnIdentifier = column.identifier = column.id || i;
102 this._columns[columnIdentifier] = column;
103 if (column.disclosure)
104 this.disclosureColumnIdentifier = columnIdentifier;
106 var cell = document.createElement("th");
107 cell.className = columnIdentifier + "-column";
108 cell.columnIdentifier = columnIdentifier;
109 this._headerTableHeaders[columnIdentifier] = cell;
111 var div = document.createElement("div");
112 if (column.titleDOMFragment)
113 div.appendChild(column.titleDOMFragment);
115 div.textContent = column.title;
116 cell.appendChild(div);
119 cell.classList.add(column.sort);
120 this._sortColumnCell = cell;
123 if (column.sortable) {
124 cell.addEventListener("click", this._clickInHeaderCell.bind(this), false);
125 cell.classList.add("sortable");
129 this._headerTable.appendChild(this._headerTableColumnGroup);
130 this.headerTableBody.appendChild(this._headerRow);
132 this._dataTable.appendChild(this._dataTableColumnGroup);
133 this.dataTableBody.appendChild(this._topFillerRow);
134 this.dataTableBody.appendChild(this._bottomFillerRow);
136 this._refreshHeader();
138 /** @type {boolean} */
139 this._editing = false;
140 /** @type {?WebInspector.DataGridNode} */
141 this.selectedNode = null;
142 /** @type {boolean} */
143 this.expandNodesWhenArrowing = false;
144 this.setRootNode(new WebInspector.DataGridNode());
145 /** @type {number} */
146 this.indentWidth = 15;
147 /** @type {!Array.<!Element|{__index: number, __position: number}>} */
149 /** @type {boolean} */
150 this._columnWidthsInitialized = false;
151 /** @type {number} */
152 this._cornerWidth = WebInspector.DataGrid.CornerWidth;
153 /** @type {!WebInspector.DataGrid.ResizeMethod} */
154 this._resizeMethod = WebInspector.DataGrid.ResizeMethod.Nearest;
157 // Keep in sync with .data-grid col.corner style rule.
158 WebInspector.DataGrid.CornerWidth = 14;
160 /** @typedef {!{id: ?string, editable: boolean, longText: ?boolean, sort: !WebInspector.DataGrid.Order, sortable: boolean, align: !WebInspector.DataGrid.Align}} */
161 WebInspector.DataGrid.ColumnDescriptor;
163 WebInspector.DataGrid.Events = {
164 SelectedNode: "SelectedNode",
165 DeselectedNode: "DeselectedNode",
166 SortingChanged: "SortingChanged",
167 ColumnsResized: "ColumnsResized"
170 /** @enum {string} */
171 WebInspector.DataGrid.Order = {
172 Ascending: "sort-ascending",
173 Descending: "sort-descending"
176 /** @enum {string} */
177 WebInspector.DataGrid.Align = {
182 WebInspector.DataGrid.prototype = {
184 * @param {string} cellClass
186 setCellClass: function(cellClass)
188 this._cellClass = cellClass;
191 _refreshHeader: function()
193 this._headerTableColumnGroup.removeChildren();
194 this._dataTableColumnGroup.removeChildren();
195 this._headerRow.removeChildren();
196 this._topFillerRow.removeChildren();
197 this._bottomFillerRow.removeChildren();
199 for (var i = 0; i < this._visibleColumnsArray.length; ++i) {
200 var column = this._visibleColumnsArray[i];
201 var columnIdentifier = column.identifier;
202 var headerColumn = this._headerTableColumnGroup.createChild("col");
203 var dataColumn = this._dataTableColumnGroup.createChild("col");
205 headerColumn.style.width = column.width;
206 dataColumn.style.width = column.width;
208 this._headerRow.appendChild(this._headerTableHeaders[columnIdentifier]);
209 this._topFillerRow.createChild("td", "top-filler-td");
210 this._bottomFillerRow.createChild("td", "bottom-filler-td");
213 this._headerRow.createChild("th", "corner");
214 this._topFillerRow.createChild("td", "corner").classList.add("top-filler-td");
215 this._bottomFillerRow.createChild("td", "corner").classList.add("bottom-filler-td");
216 this._headerTableColumnGroup.createChild("col", "corner");
217 this._dataTableColumnGroup.createChild("col", "corner");
221 * @param {number} top
222 * @param {number} bottom
225 setVerticalPadding: function(top, bottom)
227 this._topFillerRow.style.height = top + "px";
229 this._bottomFillerRow.style.height = bottom + "px";
231 this._bottomFillerRow.style.height = "auto";
235 * @param {!WebInspector.DataGridNode} rootNode
238 setRootNode: function(rootNode)
240 if (this._rootNode) {
241 this._rootNode.removeChildren();
242 this._rootNode.dataGrid = null;
243 this._rootNode._isRoot = false;
245 /** @type {!WebInspector.DataGridNode} */
246 this._rootNode = rootNode;
247 rootNode._isRoot = true;
248 rootNode.hasChildren = false;
249 rootNode._expanded = true;
250 rootNode._revealed = true;
251 rootNode.dataGrid = this;
255 * @return {!WebInspector.DataGridNode}
259 return this._rootNode;
262 _ondblclick: function(event)
264 if (this._editing || this._editingNode)
267 var columnIdentifier = this.columnIdentifierFromNode(event.target);
268 if (!columnIdentifier || !this._columns[columnIdentifier].editable)
270 this._startEditing(event.target);
274 * @param {!WebInspector.DataGridNode} node
275 * @param {number} cellIndex
277 _startEditingColumnOfDataGridNode: function(node, cellIndex)
279 this._editing = true;
280 /** @type {?WebInspector.DataGridNode} */
281 this._editingNode = node;
282 this._editingNode.select();
284 var element = this._editingNode._element.children[cellIndex];
285 WebInspector.InplaceEditor.startEditing(element, this._startEditingConfig(element));
286 window.getSelection().setBaseAndExtent(element, 0, element, 1);
289 _startEditing: function(target)
291 var element = target.enclosingNodeOrSelfWithNodeName("td");
295 this._editingNode = this.dataGridNodeFromNode(target);
296 if (!this._editingNode) {
297 if (!this.creationNode)
299 this._editingNode = this.creationNode;
302 // Force editing the 1st column when editing the creation node
303 if (this._editingNode.isCreationNode)
304 return this._startEditingColumnOfDataGridNode(this._editingNode, this._nextEditableColumn(-1));
306 this._editing = true;
307 WebInspector.InplaceEditor.startEditing(element, this._startEditingConfig(element));
309 window.getSelection().setBaseAndExtent(element, 0, element, 1);
312 renderInline: function()
314 this.element.classList.add("inline");
315 this._cornerWidth = 0;
319 _startEditingConfig: function(element)
321 return new WebInspector.InplaceEditor.Config(this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent);
324 _editingCommitted: function(element, newText, oldText, context, moveDirection)
326 var columnIdentifier = this.columnIdentifierFromNode(element);
327 if (!columnIdentifier) {
328 this._editingCancelled(element);
331 var column = this._columns[columnIdentifier];
332 var cellIndex = this._visibleColumnsArray.indexOf(column);
333 var textBeforeEditing = this._editingNode.data[columnIdentifier];
334 var currentEditingNode = this._editingNode;
337 * @param {boolean} wasChange
338 * @this {WebInspector.DataGrid}
340 function moveToNextIfNeeded(wasChange) {
344 if (moveDirection === "forward") {
345 var firstEditableColumn = this._nextEditableColumn(-1);
346 if (currentEditingNode.isCreationNode && cellIndex === firstEditableColumn && !wasChange)
349 var nextEditableColumn = this._nextEditableColumn(cellIndex);
350 if (nextEditableColumn !== -1)
351 return this._startEditingColumnOfDataGridNode(currentEditingNode, nextEditableColumn);
353 var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true);
354 if (nextDataGridNode)
355 return this._startEditingColumnOfDataGridNode(nextDataGridNode, firstEditableColumn);
356 if (currentEditingNode.isCreationNode && wasChange) {
357 this.addCreationNode(false);
358 return this._startEditingColumnOfDataGridNode(this.creationNode, firstEditableColumn);
363 if (moveDirection === "backward") {
364 var prevEditableColumn = this._nextEditableColumn(cellIndex, true);
365 if (prevEditableColumn !== -1)
366 return this._startEditingColumnOfDataGridNode(currentEditingNode, prevEditableColumn);
368 var lastEditableColumn = this._nextEditableColumn(this._visibleColumnsArray.length, true);
369 var nextDataGridNode = currentEditingNode.traversePreviousNode(true, true);
370 if (nextDataGridNode)
371 return this._startEditingColumnOfDataGridNode(nextDataGridNode, lastEditableColumn);
376 if (textBeforeEditing == newText) {
377 this._editingCancelled(element);
378 moveToNextIfNeeded.call(this, false);
382 // Update the text in the datagrid that we typed
383 this._editingNode.data[columnIdentifier] = newText;
385 // Make the callback - expects an editing node (table row), the column number that is being edited,
386 // the text that used to be there, and the new text.
387 this._editCallback(this._editingNode, columnIdentifier, textBeforeEditing, newText);
389 if (this._editingNode.isCreationNode)
390 this.addCreationNode(false);
392 this._editingCancelled(element);
393 moveToNextIfNeeded.call(this, true);
396 _editingCancelled: function(element)
398 this._editing = false;
399 this._editingNode = null;
403 * @param {number} cellIndex
404 * @param {boolean=} moveBackward
407 _nextEditableColumn: function(cellIndex, moveBackward)
409 var increment = moveBackward ? -1 : 1;
410 var columns = this._visibleColumnsArray;
411 for (var i = cellIndex + increment; (i >= 0) && (i < columns.length); i += increment) {
412 if (columns[i].editable)
421 sortColumnIdentifier: function()
423 if (!this._sortColumnCell)
425 return this._sortColumnCell.columnIdentifier;
431 sortOrder: function()
433 if (!this._sortColumnCell || this._sortColumnCell.classList.contains(WebInspector.DataGrid.Order.Ascending))
434 return WebInspector.DataGrid.Order.Ascending;
435 if (this._sortColumnCell.classList.contains(WebInspector.DataGrid.Order.Descending))
436 return WebInspector.DataGrid.Order.Descending;
443 isSortOrderAscending: function()
445 return !this._sortColumnCell || this._sortColumnCell.classList.contains(WebInspector.DataGrid.Order.Ascending);
448 get headerTableBody()
450 if ("_headerTableBody" in this)
451 return this._headerTableBody;
453 this._headerTableBody = this._headerTable.getElementsByTagName("tbody")[0];
454 if (!this._headerTableBody) {
455 this._headerTableBody = this.element.ownerDocument.createElement("tbody");
456 this._headerTable.insertBefore(this._headerTableBody, this._headerTable.tFoot);
459 return this._headerTableBody;
464 if ("_dataTableBody" in this)
465 return this._dataTableBody;
467 this._dataTableBody = this._dataTable.getElementsByTagName("tbody")[0];
468 if (!this._dataTableBody) {
469 this._dataTableBody = this.element.ownerDocument.createElement("tbody");
470 this._dataTable.insertBefore(this._dataTableBody, this._dataTable.tFoot);
473 return this._dataTableBody;
477 * @param {!Array.<number>} widths
478 * @param {number} minPercent
479 * @param {number=} maxPercent
480 * @return {!Array.<number>}
482 _autoSizeWidths: function(widths, minPercent, maxPercent)
485 minPercent = Math.min(minPercent, Math.floor(100 / widths.length));
487 for (var i = 0; i < widths.length; ++i)
488 totalWidth += widths[i];
489 var totalPercentWidth = 0;
490 for (var i = 0; i < widths.length; ++i) {
491 var width = Math.round(100 * widths[i] / totalWidth);
492 if (minPercent && width < minPercent)
494 else if (maxPercent && width > maxPercent)
496 totalPercentWidth += width;
499 var recoupPercent = totalPercentWidth - 100;
501 while (minPercent && recoupPercent > 0) {
502 for (var i = 0; i < widths.length; ++i) {
503 if (widths[i] > minPercent) {
512 while (maxPercent && recoupPercent < 0) {
513 for (var i = 0; i < widths.length; ++i) {
514 if (widths[i] < maxPercent) {
527 * @param {number} minPercent
528 * @param {number=} maxPercent
529 * @param {number=} maxDescentLevel
531 autoSizeColumns: function(minPercent, maxPercent, maxDescentLevel)
534 for (var i = 0; i < this._columnsArray.length; ++i)
535 widths.push((this._columnsArray[i].title || "").length);
537 maxDescentLevel = maxDescentLevel || 0;
538 var children = this._enumerateChildren(this._rootNode, [], maxDescentLevel + 1);
539 for (var i = 0; i < children.length; ++i) {
540 var node = children[i];
541 for (var j = 0; j < this._columnsArray.length; ++j) {
542 var text = node.data[this._columnsArray[j].identifier] || "";
543 if (text.length > widths[j])
544 widths[j] = text.length;
548 widths = this._autoSizeWidths(widths, minPercent, maxPercent);
550 for (var i = 0; i < this._columnsArray.length; ++i)
551 this._columnsArray[i].weight = widths[i];
552 this._columnWidthsInitialized = false;
556 _enumerateChildren: function(rootNode, result, maxLevel)
558 if (!rootNode._isRoot)
559 result.push(rootNode);
562 for (var i = 0; i < rootNode.children.length; ++i)
563 this._enumerateChildren(rootNode.children[i], result, maxLevel - 1);
572 // Updates the widths of the table, including the positions of the column
575 // IMPORTANT: This function MUST be called once after the element of the
576 // DataGrid is attached to its parent element and every subsequent time the
577 // width of the parent element is changed in order to make it possible to
578 // resize the columns.
580 // If this function is not called after the DataGrid is attached to its
581 // parent element, then the DataGrid's columns will not be resizable.
582 updateWidths: function()
584 var headerTableColumns = this._headerTableColumnGroup.children;
586 // Use container size to avoid changes of table width caused by change of column widths.
587 var tableWidth = this.element.offsetWidth - this._cornerWidth;
588 var numColumns = headerTableColumns.length - 1; // Do not process corner column.
590 // Do not attempt to use offsetes if we're not attached to the document tree yet.
591 if (!this._columnWidthsInitialized && this.element.offsetWidth) {
592 // Give all the columns initial widths now so that during a resize,
593 // when the two columns that get resized get a percent value for
594 // their widths, all the other columns already have percent values
596 for (var i = 0; i < numColumns; i++) {
597 var columnWidth = this.headerTableBody.rows[0].cells[i].offsetWidth;
598 var column = this._visibleColumnsArray[i];
600 column.weight = 100 * columnWidth / tableWidth;
602 this._columnWidthsInitialized = true;
604 this._applyColumnWeights();
608 * @param {string} name
610 setName: function(name)
612 this._columnWeightsSetting = WebInspector.settings.createSetting("dataGrid-" + name + "-columnWeights", {});
613 this._loadColumnWeights();
616 _loadColumnWeights: function()
618 if (!this._columnWeightsSetting)
620 var weights = this._columnWeightsSetting.get();
621 for (var i = 0; i < this._columnsArray.length; ++i) {
622 var column = this._columnsArray[i];
623 var weight = weights[column.identifier];
625 column.weight = weight;
627 this._applyColumnWeights();
630 _saveColumnWeights: function()
632 if (!this._columnWeightsSetting)
635 for (var i = 0; i < this._columnsArray.length; ++i) {
636 var column = this._columnsArray[i];
637 weights[column.identifier] = column.weight;
639 this._columnWeightsSetting.set(weights);
644 this._loadColumnWeights();
647 _applyColumnWeights: function()
649 var tableWidth = this.element.offsetWidth - this._cornerWidth;
653 var sumOfWeights = 0.0;
654 for (var i = 0; i < this._visibleColumnsArray.length; ++i)
655 sumOfWeights += this._visibleColumnsArray[i].weight;
660 for (var i = 0; i < this._visibleColumnsArray.length; ++i) {
661 sum += this._visibleColumnsArray[i].weight;
662 var offset = (sum * tableWidth / sumOfWeights) | 0;
663 var width = (offset - lastOffset) + "px";
664 this._headerTableColumnGroup.children[i].style.width = width;
665 this._dataTableColumnGroup.children[i].style.width = width;
669 this._positionResizers();
670 this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
674 * @param {!Object.<string, boolean>} columnsVisibility
676 setColumnsVisiblity: function(columnsVisibility)
678 this._visibleColumnsArray = [];
679 for (var i = 0; i < this._columnsArray.length; ++i) {
680 var column = this._columnsArray[i];
681 if (columnsVisibility[column.identifier])
682 this._visibleColumnsArray.push(column);
684 this._refreshHeader();
685 this._applyColumnWeights();
686 var nodes = this._enumerateChildren(this.rootNode(), [], -1);
687 for (var i = 0; i < nodes.length; ++i)
691 get scrollContainer()
693 return this._scrollContainer;
696 _positionResizers: function()
698 var headerTableColumns = this._headerTableColumnGroup.children;
699 var numColumns = headerTableColumns.length - 1; // Do not process corner column.
701 var resizers = this._resizers;
703 while (resizers.length > numColumns - 1)
704 resizers.pop().remove();
706 for (var i = 0; i < numColumns - 1; i++) {
707 // Get the width of the cell in the first (and only) row of the
708 // header table in order to determine the width of the column, since
709 // it is not possible to query a column for its width.
710 left[i] = (left[i-1] || 0) + this.headerTableBody.rows[0].cells[i].offsetWidth;
713 // Make n - 1 resizers for n columns.
714 for (var i = 0; i < numColumns - 1; i++) {
715 var resizer = resizers[i];
717 // This is the first call to updateWidth, so the resizers need
719 resizer = document.createElement("div");
721 resizer.classList.add("data-grid-resizer");
722 // This resizer is associated with the column to its right.
723 WebInspector.installDragHandle(resizer, this._startResizerDragging.bind(this), this._resizerDragging.bind(this), this._endResizerDragging.bind(this), "col-resize");
724 this.element.appendChild(resizer);
725 resizers.push(resizer);
727 if (resizer.__position !== left[i]) {
728 resizer.__position = left[i];
729 resizer.style.left = left[i] + "px";
734 addCreationNode: function(hasChildren)
736 if (this.creationNode)
737 this.creationNode.makeNormal();
740 for (var column in this._columns)
741 emptyData[column] = null;
742 this.creationNode = new WebInspector.CreationDataGridNode(emptyData, hasChildren);
743 this.rootNode().appendChild(this.creationNode);
746 _keyDown: function(event)
748 if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing)
752 var nextSelectedNode;
753 if (event.keyIdentifier === "Up" && !event.altKey) {
754 nextSelectedNode = this.selectedNode.traversePreviousNode(true);
755 while (nextSelectedNode && !nextSelectedNode.selectable)
756 nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
757 handled = nextSelectedNode ? true : false;
758 } else if (event.keyIdentifier === "Down" && !event.altKey) {
759 nextSelectedNode = this.selectedNode.traverseNextNode(true);
760 while (nextSelectedNode && !nextSelectedNode.selectable)
761 nextSelectedNode = nextSelectedNode.traverseNextNode(true);
762 handled = nextSelectedNode ? true : false;
763 } else if (event.keyIdentifier === "Left") {
764 if (this.selectedNode.expanded) {
766 this.selectedNode.collapseRecursively();
768 this.selectedNode.collapse();
770 } else if (this.selectedNode.parent && !this.selectedNode.parent._isRoot) {
772 if (this.selectedNode.parent.selectable) {
773 nextSelectedNode = this.selectedNode.parent;
774 handled = nextSelectedNode ? true : false;
775 } else if (this.selectedNode.parent)
776 this.selectedNode.parent.collapse();
778 } else if (event.keyIdentifier === "Right") {
779 if (!this.selectedNode.revealed) {
780 this.selectedNode.reveal();
782 } else if (this.selectedNode.hasChildren) {
784 if (this.selectedNode.expanded) {
785 nextSelectedNode = this.selectedNode.children[0];
786 handled = nextSelectedNode ? true : false;
789 this.selectedNode.expandRecursively();
791 this.selectedNode.expand();
794 } else if (event.keyCode === 8 || event.keyCode === 46) {
795 if (this._deleteCallback) {
797 this._deleteCallback(this.selectedNode);
798 this.changeNodeAfterDeletion();
800 } else if (isEnterKey(event)) {
801 if (this._editCallback) {
803 this._startEditing(this.selectedNode._element.children[this._nextEditableColumn(-1)]);
807 if (nextSelectedNode) {
808 nextSelectedNode.reveal();
809 nextSelectedNode.select();
816 changeNodeAfterDeletion: function()
818 var nextSelectedNode = this.selectedNode.traverseNextNode(true);
819 while (nextSelectedNode && !nextSelectedNode.selectable)
820 nextSelectedNode = nextSelectedNode.traverseNextNode(true);
822 if (!nextSelectedNode || nextSelectedNode.isCreationNode) {
823 nextSelectedNode = this.selectedNode.traversePreviousNode(true);
824 while (nextSelectedNode && !nextSelectedNode.selectable)
825 nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
828 if (nextSelectedNode) {
829 nextSelectedNode.reveal();
830 nextSelectedNode.select();
835 * @param {!Node} target
836 * @return {?WebInspector.DataGridNode}
838 dataGridNodeFromNode: function(target)
840 var rowElement = target.enclosingNodeOrSelfWithNodeName("tr");
841 return rowElement && rowElement._dataGridNode;
845 * @param {!Node} target
848 columnIdentifierFromNode: function(target)
850 var cellElement = target.enclosingNodeOrSelfWithNodeName("td");
851 return cellElement && cellElement.columnIdentifier_;
854 _clickInHeaderCell: function(event)
856 var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
857 if (!cell || (cell.columnIdentifier === undefined) || !cell.classList.contains("sortable"))
860 var sortOrder = WebInspector.DataGrid.Order.Ascending;
861 if ((cell === this._sortColumnCell) && this.isSortOrderAscending())
862 sortOrder = WebInspector.DataGrid.Order.Descending;
864 if (this._sortColumnCell)
865 this._sortColumnCell.classList.remove(WebInspector.DataGrid.Order.Ascending, WebInspector.DataGrid.Order.Descending);
866 this._sortColumnCell = cell;
868 cell.classList.add(sortOrder);
870 this.dispatchEventToListeners(WebInspector.DataGrid.Events.SortingChanged);
874 * @param {string} columnIdentifier
875 * @param {!WebInspector.DataGrid.Order} sortOrder
877 markColumnAsSortedBy: function(columnIdentifier, sortOrder)
879 if (this._sortColumnCell)
880 this._sortColumnCell.classList.remove(WebInspector.DataGrid.Order.Ascending, WebInspector.DataGrid.Order.Descending);
881 this._sortColumnCell = this._headerTableHeaders[columnIdentifier];
882 this._sortColumnCell.classList.add(sortOrder);
886 * @param {string} columnIdentifier
889 headerTableHeader: function(columnIdentifier)
891 return this._headerTableHeaders[columnIdentifier];
894 _mouseDownInDataTable: function(event)
896 var gridNode = this.dataGridNodeFromNode(event.target);
897 if (!gridNode || !gridNode.selectable)
900 if (gridNode.isEventWithinDisclosureTriangle(event))
904 if (gridNode.selected)
912 _contextMenuInDataTable: function(event)
914 var contextMenu = new WebInspector.ContextMenu(event);
916 var gridNode = this.dataGridNodeFromNode(event.target);
917 if (this._refreshCallback && (!gridNode || gridNode !== this.creationNode))
918 contextMenu.appendItem(WebInspector.UIString("Refresh"), this._refreshCallback.bind(this));
920 if (gridNode && gridNode.selectable && !gridNode.isEventWithinDisclosureTriangle(event)) {
921 if (this._editCallback) {
922 if (gridNode === this.creationNode)
923 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add new" : "Add New"), this._startEditing.bind(this, event.target));
925 var columnIdentifier = this.columnIdentifierFromNode(event.target);
926 if (columnIdentifier && this._columns[columnIdentifier].editable)
927 contextMenu.appendItem(WebInspector.UIString("Edit \"%s\"", this._columns[columnIdentifier].title), this._startEditing.bind(this, event.target));
930 if (this._deleteCallback && gridNode !== this.creationNode)
931 contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode));
932 if (this._contextMenuCallback)
933 this._contextMenuCallback(contextMenu, gridNode);
939 _clickInDataTable: function(event)
941 var gridNode = this.dataGridNodeFromNode(event.target);
942 if (!gridNode || !gridNode.hasChildren)
945 if (!gridNode.isEventWithinDisclosureTriangle(event))
948 if (gridNode.expanded) {
950 gridNode.collapseRecursively();
955 gridNode.expandRecursively();
962 * @param {!WebInspector.DataGrid.ResizeMethod} method
964 setResizeMethod: function(method)
966 this._resizeMethod = method;
972 _startResizerDragging: function(event)
974 this._currentResizer = event.target;
978 _resizerDragging: function(event)
980 var resizer = this._currentResizer;
984 var tableWidth = this.element.offsetWidth; // Cache it early, before we invalidate layout.
986 // Constrain the dragpoint to be within the containing div of the
988 var dragPoint = event.clientX - this.element.totalOffsetLeft();
989 var firstRowCells = this.headerTableBody.rows[0].cells;
990 var leftEdgeOfPreviousColumn = 0;
991 // Constrain the dragpoint to be within the space made up by the
992 // column directly to the left and the column directly to the right.
993 var leftCellIndex = resizer.__index;
994 var rightCellIndex = leftCellIndex + 1;
995 for (var i = 0; i < leftCellIndex; i++)
996 leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth;
998 // Differences for other resize methods
999 if (this._resizeMethod === WebInspector.DataGrid.ResizeMethod.Last) {
1000 rightCellIndex = this._resizers.length;
1001 } else if (this._resizeMethod === WebInspector.DataGrid.ResizeMethod.First) {
1002 leftEdgeOfPreviousColumn += firstRowCells[leftCellIndex].offsetWidth - firstRowCells[0].offsetWidth;
1006 var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[leftCellIndex].offsetWidth + firstRowCells[rightCellIndex].offsetWidth;
1008 // Give each column some padding so that they don't disappear.
1009 var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding;
1010 var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding;
1011 if (leftMinimum > rightMaximum)
1014 dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum);
1016 var position = (dragPoint - this.CenterResizerOverBorderAdjustment);
1017 resizer.__position = position;
1018 resizer.style.left = position + "px";
1020 var pxLeftColumn = (dragPoint - leftEdgeOfPreviousColumn) + "px";
1021 this._headerTableColumnGroup.children[leftCellIndex].style.width = pxLeftColumn;
1022 this._dataTableColumnGroup.children[leftCellIndex].style.width = pxLeftColumn;
1024 var pxRightColumn = (rightEdgeOfNextColumn - dragPoint) + "px";
1025 this._headerTableColumnGroup.children[rightCellIndex].style.width = pxRightColumn;
1026 this._dataTableColumnGroup.children[rightCellIndex].style.width = pxRightColumn;
1028 var leftColumn = this._visibleColumnsArray[leftCellIndex];
1029 var rightColumn = this._visibleColumnsArray[rightCellIndex];
1030 if (leftColumn.weight || rightColumn.weight) {
1031 var sumOfWeights = leftColumn.weight + rightColumn.weight;
1032 var delta = rightEdgeOfNextColumn - leftEdgeOfPreviousColumn;
1033 leftColumn.weight = (dragPoint - leftEdgeOfPreviousColumn) * sumOfWeights / delta;
1034 rightColumn.weight = (rightEdgeOfNextColumn - dragPoint) * sumOfWeights / delta;
1037 this._positionResizers();
1038 event.preventDefault();
1039 this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
1043 * @param {string} columnId
1046 columnOffset: function(columnId)
1048 if (!this.element.offsetWidth)
1050 for (var i = 1; i < this._visibleColumnsArray.length; ++i) {
1051 if (columnId === this._visibleColumnsArray[i].identifier) {
1052 if (this._resizers[i - 1])
1053 return this._resizers[i - 1].__position;
1059 _endResizerDragging: function(event)
1061 this._currentResizer = null;
1062 this._saveColumnWeights();
1063 this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
1066 ColumnResizePadding: 24,
1068 CenterResizerOverBorderAdjustment: 3,
1070 __proto__: WebInspector.View.prototype
1073 /** @enum {string} */
1074 WebInspector.DataGrid.ResizeMethod = {
1082 * @extends {WebInspector.Object}
1083 * @param {?Object.<string, *>=} data
1084 * @param {boolean=} hasChildren
1086 WebInspector.DataGridNode = function(data, hasChildren)
1088 /** @type {?Element} */
1089 this._element = null;
1090 /** @type {boolean} */
1091 this._expanded = false;
1092 /** @type {boolean} */
1093 this._selected = false;
1094 /** @type {number|undefined} */
1096 /** @type {boolean|undefined} */
1098 /** @type {boolean} */
1099 this._attached = false;
1100 /** @type {?{parent: !WebInspector.DataGridNode, index: number}} */
1101 this._savedPosition = null;
1102 /** @type {boolean} */
1103 this._shouldRefreshChildren = true;
1104 /** @type {!Object.<string, *>} */
1105 this._data = data || {};
1106 /** @type {boolean} */
1107 this.hasChildren = hasChildren || false;
1108 /** @type {!Array.<!WebInspector.DataGridNode>} */
1110 /** @type {?WebInspector.DataGrid} */
1111 this.dataGrid = null;
1112 /** @type {?WebInspector.DataGridNode} */
1114 /** @type {?WebInspector.DataGridNode} */
1115 this.previousSibling = null;
1116 /** @type {?WebInspector.DataGridNode} */
1117 this.nextSibling = null;
1118 /** @type {number} */
1119 this.disclosureToggleWidth = 10;
1122 WebInspector.DataGridNode.prototype = {
1123 /** @type {boolean} */
1126 /** @type {boolean} */
1130 * @return {!Element}
1134 if (!this._element) {
1135 this.createElement();
1138 return /** @type {!Element} */ (this._element);
1144 createElement: function()
1146 this._element = document.createElement("tr");
1147 this._element._dataGridNode = this;
1149 if (this.hasChildren)
1150 this._element.classList.add("parent");
1152 this._element.classList.add("expanded");
1154 this._element.classList.add("selected");
1156 this._element.classList.add("revealed");
1162 createCells: function()
1164 this._element.removeChildren();
1165 var columnsArray = this.dataGrid._visibleColumnsArray;
1166 for (var i = 0; i < columnsArray.length; ++i)
1167 this._element.appendChild(this.createCell(columnsArray[i].identifier));
1168 this._element.appendChild(this._createTDWithClass("corner"));
1178 this._data = x || {};
1184 if (this._revealed !== undefined)
1185 return this._revealed;
1187 var currentAncestor = this.parent;
1188 while (currentAncestor && !currentAncestor._isRoot) {
1189 if (!currentAncestor.expanded) {
1190 this._revealed = false;
1194 currentAncestor = currentAncestor.parent;
1197 this._revealed = true;
1203 if (this._hasChildren === x)
1206 this._hasChildren = x;
1211 this._element.classList.toggle("parent", this._hasChildren);
1212 this._element.classList.toggle("expanded", this._hasChildren && this.expanded);
1217 return this._hasChildren;
1222 if (this._revealed === x)
1228 this._element.classList.toggle("revealed", this._revealed);
1230 for (var i = 0; i < this.children.length; ++i)
1231 this.children[i].revealed = x && this.expanded;
1239 if (this._depth !== undefined)
1241 if (this.parent && !this.parent._isRoot)
1242 this._depth = this.parent.depth + 1;
1250 if (typeof this._leftPadding === "number")
1251 return this._leftPadding;
1253 this._leftPadding = this.depth * this.dataGrid.indentWidth;
1254 return this._leftPadding;
1257 get shouldRefreshChildren()
1259 return this._shouldRefreshChildren;
1262 set shouldRefreshChildren(x)
1264 this._shouldRefreshChildren = x;
1265 if (x && this.expanded)
1271 return this._selected;
1284 return this._expanded;
1288 * @param {boolean} x
1301 this._element = null;
1308 * @param {string} className
1309 * @return {!Element}
1311 _createTDWithClass: function(className)
1313 var cell = document.createElementWithClass("td", className);
1314 var cellClass = this.dataGrid._cellClass;
1316 cell.classList.add(cellClass);
1321 * @param {string} columnIdentifier
1322 * @return {!Element}
1324 createTD: function(columnIdentifier)
1326 var cell = this._createTDWithClass(columnIdentifier + "-column");
1327 cell.columnIdentifier_ = columnIdentifier;
1329 var alignment = this.dataGrid._columns[columnIdentifier].align;
1331 cell.classList.add(alignment);
1337 * @param {string} columnIdentifier
1338 * @return {!Element}
1340 createCell: function(columnIdentifier)
1342 var cell = this.createTD(columnIdentifier);
1344 var data = this.data[columnIdentifier];
1345 if (data instanceof Node) {
1346 cell.appendChild(data);
1348 cell.textContent = data;
1349 if (this.dataGrid._columns[columnIdentifier].longText)
1353 if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
1354 cell.classList.add("disclosure");
1355 if (this.leftPadding)
1356 cell.style.setProperty("padding-left", this.leftPadding + "px");
1365 nodeSelfHeight: function()
1371 * @param {!WebInspector.DataGridNode} child
1373 appendChild: function(child)
1375 this.insertChild(child, this.children.length);
1379 * @param {!WebInspector.DataGridNode} child
1380 * @param {number} index
1382 insertChild: function(child, index)
1385 throw("insertChild: Node can't be undefined or null.");
1386 if (child.parent === this)
1387 throw("insertChild: Node is already a child of this node.");
1390 child.parent.removeChild(child);
1392 this.children.splice(index, 0, child);
1393 this.hasChildren = true;
1395 child.parent = this;
1396 child.dataGrid = this.dataGrid;
1397 child.recalculateSiblings(index);
1399 child._depth = undefined;
1400 child._revealed = undefined;
1401 child._attached = false;
1402 child._shouldRefreshChildren = true;
1404 var current = child.children[0];
1406 current.dataGrid = this.dataGrid;
1407 current._depth = undefined;
1408 current._revealed = undefined;
1409 current._attached = false;
1410 current._shouldRefreshChildren = true;
1411 current = current.traverseNextNode(false, child, true);
1417 child.revealed = false;
1421 * @param {!WebInspector.DataGridNode} child
1423 removeChild: function(child)
1426 throw("removeChild: Node can't be undefined or null.");
1427 if (child.parent !== this)
1428 throw("removeChild: Node is not a child of this node.");
1433 this.children.remove(child, true);
1435 if (child.previousSibling)
1436 child.previousSibling.nextSibling = child.nextSibling;
1437 if (child.nextSibling)
1438 child.nextSibling.previousSibling = child.previousSibling;
1440 child.dataGrid = null;
1441 child.parent = null;
1442 child.nextSibling = null;
1443 child.previousSibling = null;
1445 if (this.children.length <= 0)
1446 this.hasChildren = false;
1449 removeChildren: function()
1451 for (var i = 0; i < this.children.length; ++i) {
1452 var child = this.children[i];
1456 child.dataGrid = null;
1457 child.parent = null;
1458 child.nextSibling = null;
1459 child.previousSibling = null;
1463 this.hasChildren = false;
1467 * @param {number} myIndex
1469 recalculateSiblings: function(myIndex)
1474 var previousChild = this.parent.children[myIndex - 1] || null;
1476 previousChild.nextSibling = this;
1477 this.previousSibling = previousChild;
1479 var nextChild = this.parent.children[myIndex + 1] || null;
1481 nextChild.previousSibling = this;
1482 this.nextSibling = nextChild;
1485 collapse: function()
1490 this._element.classList.remove("expanded");
1492 this._expanded = false;
1494 for (var i = 0; i < this.children.length; ++i)
1495 this.children[i].revealed = false;
1498 collapseRecursively: function()
1504 item = item.traverseNextNode(false, this, true);
1508 populate: function() { },
1512 if (!this.hasChildren || this.expanded)
1517 if (this.revealed && !this._shouldRefreshChildren)
1518 for (var i = 0; i < this.children.length; ++i)
1519 this.children[i].revealed = true;
1521 if (this._shouldRefreshChildren) {
1522 for (var i = 0; i < this.children.length; ++i)
1523 this.children[i]._detach();
1527 if (this._attached) {
1528 for (var i = 0; i < this.children.length; ++i) {
1529 var child = this.children[i];
1531 child.revealed = true;
1536 this._shouldRefreshChildren = false;
1540 this._element.classList.add("expanded");
1542 this._expanded = true;
1545 expandRecursively: function()
1550 item = item.traverseNextNode(false, this);
1558 var currentAncestor = this.parent;
1559 while (currentAncestor && !currentAncestor._isRoot) {
1560 if (!currentAncestor.expanded)
1561 currentAncestor.expand();
1562 currentAncestor = currentAncestor.parent;
1565 this.element().scrollIntoViewIfNeeded(false);
1569 * @param {boolean=} supressSelectedEvent
1571 select: function(supressSelectedEvent)
1573 if (!this.dataGrid || !this.selectable || this.selected)
1576 if (this.dataGrid.selectedNode)
1577 this.dataGrid.selectedNode.deselect();
1579 this._selected = true;
1580 this.dataGrid.selectedNode = this;
1583 this._element.classList.add("selected");
1585 if (!supressSelectedEvent)
1586 this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Events.SelectedNode);
1589 revealAndSelect: function()
1598 * @param {boolean=} supressDeselectedEvent
1600 deselect: function(supressDeselectedEvent)
1602 if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
1605 this._selected = false;
1606 this.dataGrid.selectedNode = null;
1609 this._element.classList.remove("selected");
1611 if (!supressDeselectedEvent)
1612 this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Events.DeselectedNode);
1616 * @param {boolean} skipHidden
1617 * @param {?WebInspector.DataGridNode=} stayWithin
1618 * @param {boolean=} dontPopulate
1619 * @param {!Object=} info
1620 * @return {?WebInspector.DataGridNode}
1622 traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info)
1624 if (!dontPopulate && this.hasChildren)
1628 info.depthChange = 0;
1630 var node = (!skipHidden || this.revealed) ? this.children[0] : null;
1631 if (node && (!skipHidden || this.expanded)) {
1633 info.depthChange = 1;
1637 if (this === stayWithin)
1640 node = (!skipHidden || this.revealed) ? this.nextSibling : null;
1645 while (node && !node._isRoot && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) {
1647 info.depthChange -= 1;
1654 return (!skipHidden || node.revealed) ? node.nextSibling : null;
1658 * @param {boolean} skipHidden
1659 * @param {boolean=} dontPopulate
1660 * @return {?WebInspector.DataGridNode}
1662 traversePreviousNode: function(skipHidden, dontPopulate)
1664 var node = (!skipHidden || this.revealed) ? this.previousSibling : null;
1665 if (!dontPopulate && node && node.hasChildren)
1668 while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null)) {
1669 if (!dontPopulate && node.hasChildren)
1671 node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null);
1677 if (!this.parent || this.parent._isRoot)
1686 isEventWithinDisclosureTriangle: function(event)
1688 if (!this.hasChildren)
1690 var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
1691 if (!cell.classList.contains("disclosure"))
1694 var left = cell.totalOffsetLeft() + this.leftPadding;
1695 return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth;
1700 if (!this.dataGrid || this._attached)
1703 this._attached = true;
1705 var nextNode = null;
1706 var previousNode = this.traversePreviousNode(true, true);
1707 var previousElement = previousNode ? previousNode.element() : this.dataGrid._topFillerRow;
1708 this.dataGrid.dataTableBody.insertBefore(this.element(), previousElement.nextSibling);
1711 for (var i = 0; i < this.children.length; ++i)
1712 this.children[i]._attach();
1717 if (!this._attached)
1720 this._attached = false;
1723 this._element.remove();
1725 for (var i = 0; i < this.children.length; ++i)
1726 this.children[i]._detach();
1731 wasDetached: function()
1735 savePosition: function()
1737 if (this._savedPosition)
1741 throw("savePosition: Node must have a parent.");
1742 this._savedPosition = {
1743 parent: this.parent,
1744 index: this.parent.children.indexOf(this)
1748 restorePosition: function()
1750 if (!this._savedPosition)
1753 if (this.parent !== this._savedPosition.parent)
1754 this._savedPosition.parent.insertChild(this, this._savedPosition.index);
1756 this._savedPosition = null;
1759 __proto__: WebInspector.Object.prototype
1764 * @extends {WebInspector.DataGridNode}
1766 WebInspector.CreationDataGridNode = function(data, hasChildren)
1768 WebInspector.DataGridNode.call(this, data, hasChildren);
1769 /** @type {boolean} */
1770 this.isCreationNode = true;
1773 WebInspector.CreationDataGridNode.prototype = {
1774 makeNormal: function()
1776 this.isCreationNode = false;
1779 __proto__: WebInspector.DataGridNode.prototype