/* | |
* Copyright (C) 2008 Apple Inc. All Rights Reserved. | |
* | |
* Redistribution and use in source and binary forms, with or without | |
* modification, are permitted provided that the following conditions | |
* are met: | |
* 1. Redistributions of source code must retain the above copyright | |
* notice, this list of conditions and the following disclaimer. | |
* 2. Redistributions in binary form must reproduce the above copyright | |
* notice, this list of conditions and the following disclaimer in the | |
* documentation and/or other materials provided with the distribution. | |
* | |
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY | |
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR | |
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
*/ | |
WebInspector.DataGrid = function(columns, editCallback, deleteCallback) | |
{ | |
this.element = document.createElement("div"); | |
this.element.className = "data-grid"; | |
this.element.tabIndex = 0; | |
this.element.addEventListener("keydown", this._keyDown.bind(this), false); | |
this._headerTable = document.createElement("table"); | |
this._headerTable.className = "header"; | |
this._dataTable = document.createElement("table"); | |
this._dataTable.className = "data"; | |
this._dataTable.addEventListener("mousedown", this._mouseDownInDataTable.bind(this), true); | |
this._dataTable.addEventListener("click", this._clickInDataTable.bind(this), true); | |
this._dataTable.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true); | |
// FIXME: Add a createCallback which is different from editCallback and has different | |
// behavior when creating a new node. | |
if (editCallback) { | |
this._dataTable.addEventListener("dblclick", this._ondblclick.bind(this), false); | |
this._editCallback = editCallback; | |
} | |
if (deleteCallback) | |
this._deleteCallback = deleteCallback; | |
this.aligned = {}; | |
var scrollContainer = document.createElement("div"); | |
scrollContainer.className = "data-container"; | |
scrollContainer.appendChild(this._dataTable); | |
this.element.appendChild(this._headerTable); | |
this.element.appendChild(scrollContainer); | |
var headerRow = document.createElement("tr"); | |
var columnGroup = document.createElement("colgroup"); | |
this._columnCount = 0; | |
for (var columnIdentifier in columns) { | |
var column = columns[columnIdentifier]; | |
if (column.disclosure) | |
this.disclosureColumnIdentifier = columnIdentifier; | |
var col = document.createElement("col"); | |
if (column.width) | |
col.style.width = column.width; | |
column.element = col; | |
columnGroup.appendChild(col); | |
var cell = document.createElement("th"); | |
cell.className = columnIdentifier + "-column"; | |
cell.columnIdentifier = columnIdentifier; | |
var div = document.createElement("div"); | |
div.textContent = column.title; | |
cell.appendChild(div); | |
if (column.sort) { | |
cell.addStyleClass("sort-" + column.sort); | |
this._sortColumnCell = cell; | |
} | |
if (column.sortable) { | |
cell.addEventListener("click", this._clickInHeaderCell.bind(this), false); | |
cell.addStyleClass("sortable"); | |
} | |
if (column.aligned) { | |
cell.addStyleClass(column.aligned); | |
this.aligned[columnIdentifier] = column.aligned; | |
} | |
headerRow.appendChild(cell); | |
++this._columnCount; | |
} | |
columnGroup.span = this._columnCount; | |
var cell = document.createElement("th"); | |
cell.className = "corner"; | |
headerRow.appendChild(cell); | |
this._headerTableColumnGroup = columnGroup; | |
this._headerTable.appendChild(this._headerTableColumnGroup); | |
this.headerTableBody.appendChild(headerRow); | |
var fillerRow = document.createElement("tr"); | |
fillerRow.className = "filler"; | |
for (var i = 0; i < this._columnCount; ++i) { | |
var cell = document.createElement("td"); | |
fillerRow.appendChild(cell); | |
} | |
this._dataTableColumnGroup = columnGroup.cloneNode(true); | |
this._dataTable.appendChild(this._dataTableColumnGroup); | |
this.dataTableBody.appendChild(fillerRow); | |
this.columns = columns || {}; | |
this.children = []; | |
this.selectedNode = null; | |
this.expandNodesWhenArrowing = false; | |
this.root = true; | |
this.hasChildren = false; | |
this.expanded = true; | |
this.revealed = true; | |
this.selected = false; | |
this.dataGrid = this; | |
this.indentWidth = 15; | |
this.resizers = []; | |
this.columnWidthsInitialized = false; | |
} | |
WebInspector.DataGrid.prototype = { | |
_ondblclick: function(event) | |
{ | |
if (this._editing || this._editingNode) | |
return; | |
this._startEditing(event.target); | |
}, | |
_startEditingColumnOfDataGridNode: function(node, column) | |
{ | |
this._editing = true; | |
this._editingNode = node; | |
this._editingNode.select(); | |
var element = this._editingNode._element.children[column]; | |
WebInspector.startEditing(element, this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent); | |
window.getSelection().setBaseAndExtent(element, 0, element, 1); | |
}, | |
_startEditing: function(target) | |
{ | |
var element = target.enclosingNodeOrSelfWithNodeName("td"); | |
if (!element) | |
return; | |
this._editingNode = this.dataGridNodeFromNode(target); | |
if (!this._editingNode) { | |
if (!this.creationNode) | |
return; | |
this._editingNode = this.creationNode; | |
} | |
// Force editing the 1st column when editing the creation node | |
if (this._editingNode.isCreationNode) | |
return this._startEditingColumnOfDataGridNode(this._editingNode, 0); | |
this._editing = true; | |
WebInspector.startEditing(element, this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent); | |
window.getSelection().setBaseAndExtent(element, 0, element, 1); | |
}, | |
_editingCommitted: function(element, newText, oldText, context, moveDirection) | |
{ | |
// FIXME: We need more column identifiers here throughout this function. | |
// Not needed yet since only editable DataGrid is DOM Storage, which is Key - Value. | |
// FIXME: Better way to do this than regular expressions? | |
var columnIdentifier = parseInt(element.className.match(/\b(\d+)-column\b/)[1]); | |
var textBeforeEditing = this._editingNode.data[columnIdentifier]; | |
var currentEditingNode = this._editingNode; | |
function moveToNextIfNeeded(wasChange) { | |
if (!moveDirection) | |
return; | |
if (moveDirection === "forward") { | |
if (currentEditingNode.isCreationNode && columnIdentifier === 0 && !wasChange) | |
return; | |
if (columnIdentifier === 0) | |
return this._startEditingColumnOfDataGridNode(currentEditingNode, 1); | |
var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true); | |
if (nextDataGridNode) | |
return this._startEditingColumnOfDataGridNode(nextDataGridNode, 0); | |
if (currentEditingNode.isCreationNode && wasChange) { | |
addCreationNode(false); | |
return this._startEditingColumnOfDataGridNode(this.creationNode, 0); | |
} | |
return; | |
} | |
if (moveDirection === "backward") { | |
if (columnIdentifier === 1) | |
return this._startEditingColumnOfDataGridNode(currentEditingNode, 0); | |
var nextDataGridNode = currentEditingNode.traversePreviousNode(true, null, true); | |
if (nextDataGridNode) | |
return this._startEditingColumnOfDataGridNode(nextDataGridNode, 1); | |
return; | |
} | |
} | |
if (textBeforeEditing == newText) { | |
this._editingCancelled(element); | |
moveToNextIfNeeded.call(this, false); | |
return; | |
} | |
// Update the text in the datagrid that we typed | |
this._editingNode.data[columnIdentifier] = newText; | |
// Make the callback - expects an editing node (table row), the column number that is being edited, | |
// the text that used to be there, and the new text. | |
this._editCallback(this._editingNode, columnIdentifier, textBeforeEditing, newText); | |
if (this._editingNode.isCreationNode) | |
this.addCreationNode(false); | |
this._editingCancelled(element); | |
moveToNextIfNeeded.call(this, true); | |
}, | |
_editingCancelled: function(element, context) | |
{ | |
delete this._editing; | |
this._editingNode = null; | |
}, | |
get sortColumnIdentifier() | |
{ | |
if (!this._sortColumnCell) | |
return null; | |
return this._sortColumnCell.columnIdentifier; | |
}, | |
get sortOrder() | |
{ | |
if (!this._sortColumnCell || this._sortColumnCell.hasStyleClass("sort-ascending")) | |
return "ascending"; | |
if (this._sortColumnCell.hasStyleClass("sort-descending")) | |
return "descending"; | |
return null; | |
}, | |
get headerTableBody() | |
{ | |
if ("_headerTableBody" in this) | |
return this._headerTableBody; | |
this._headerTableBody = this._headerTable.getElementsByTagName("tbody")[0]; | |
if (!this._headerTableBody) { | |
this._headerTableBody = this.element.ownerDocument.createElement("tbody"); | |
this._headerTable.insertBefore(this._headerTableBody, this._headerTable.tFoot); | |
} | |
return this._headerTableBody; | |
}, | |
get dataTableBody() | |
{ | |
if ("_dataTableBody" in this) | |
return this._dataTableBody; | |
this._dataTableBody = this._dataTable.getElementsByTagName("tbody")[0]; | |
if (!this._dataTableBody) { | |
this._dataTableBody = this.element.ownerDocument.createElement("tbody"); | |
this._dataTable.insertBefore(this._dataTableBody, this._dataTable.tFoot); | |
} | |
return this._dataTableBody; | |
}, | |
autoSizeColumns: function(minPercent, maxPercent) | |
{ | |
if (minPercent) | |
minPercent = Math.min(minPercent, Math.floor(100 / this._columnCount)); | |
var widths = {}; | |
var columns = this.columns; | |
for (var columnIdentifier in columns) | |
widths[columnIdentifier] = (columns[columnIdentifier].title || "").length; | |
for (var i = 0; i < this.children.length; ++i) { | |
var node = this.children[i]; | |
for (var columnIdentifier in columns) { | |
var text = node.data[columnIdentifier] || ""; | |
if (text.length > widths[columnIdentifier]) | |
widths[columnIdentifier] = text.length; | |
} | |
} | |
var totalColumnWidths = 0; | |
for (var columnIdentifier in columns) | |
totalColumnWidths += widths[columnIdentifier]; | |
var recoupPercent = 0; | |
for (var columnIdentifier in columns) { | |
var width = Math.round(100 * widths[columnIdentifier] / totalColumnWidths); | |
if (minPercent && width < minPercent) { | |
recoupPercent += (minPercent - width); | |
width = minPercent; | |
} else if (maxPercent && width > maxPercent) { | |
recoupPercent -= (width - maxPercent); | |
width = maxPercent; | |
} | |
widths[columnIdentifier] = width; | |
} | |
while (minPercent && recoupPercent > 0) { | |
for (var columnIdentifier in columns) { | |
if (widths[columnIdentifier] > minPercent) { | |
--widths[columnIdentifier]; | |
--recoupPercent; | |
if (!recoupPercent) | |
break; | |
} | |
} | |
} | |
while (maxPercent && recoupPercent < 0) { | |
for (var columnIdentifier in columns) { | |
if (widths[columnIdentifier] < maxPercent) { | |
++widths[columnIdentifier]; | |
++recoupPercent; | |
if (!recoupPercent) | |
break; | |
} | |
} | |
} | |
for (var columnIdentifier in columns) | |
columns[columnIdentifier].element.style.width = widths[columnIdentifier] + "%"; | |
this.columnWidthsInitialized = false; | |
this.updateWidths(); | |
}, | |
// Updates the widths of the table, including the positions of the column | |
// resizers. | |
// | |
// IMPORTANT: This function MUST be called once after the element of the | |
// DataGrid is attached to its parent element and every subsequent time the | |
// width of the parent element is changed in order to make it possible to | |
// resize the columns. | |
// | |
// If this function is not called after the DataGrid is attached to its | |
// parent element, then the DataGrid's columns will not be resizable. | |
updateWidths: function() | |
{ | |
var headerTableColumns = this._headerTableColumnGroup.children; | |
var left = 0; | |
var tableWidth = this._dataTable.offsetWidth; | |
var numColumns = headerTableColumns.length; | |
if (!this.columnWidthsInitialized) { | |
// Give all the columns initial widths now so that during a resize, | |
// when the two columns that get resized get a percent value for | |
// their widths, all the other columns already have percent values | |
// for their widths. | |
for (var i = 0; i < numColumns; i++) { | |
var columnWidth = this.headerTableBody.rows[0].cells[i].offsetWidth; | |
var percentWidth = ((columnWidth / tableWidth) * 100) + "%"; | |
this._headerTableColumnGroup.children[i].style.width = percentWidth; | |
this._dataTableColumnGroup.children[i].style.width = percentWidth; | |
} | |
this.columnWidthsInitialized = true; | |
} | |
// Make n - 1 resizers for n columns. | |
for (var i = 0; i < numColumns - 1; i++) { | |
var resizer = this.resizers[i]; | |
if (!resizer) { | |
// This is the first call to updateWidth, so the resizers need | |
// to be created. | |
resizer = document.createElement("div"); | |
resizer.addStyleClass("data-grid-resizer"); | |
// This resizer is associated with the column to its right. | |
resizer.rightNeighboringColumnID = i + 1; | |
resizer.addEventListener("mousedown", this._startResizerDragging.bind(this), false); | |
this.element.appendChild(resizer); | |
this.resizers[i] = resizer; | |
} | |
// Get the width of the cell in the first (and only) row of the | |
// header table in order to determine the width of the column, since | |
// it is not possible to query a column for its width. | |
left += this.headerTableBody.rows[0].cells[i].offsetWidth; | |
resizer.style.left = left + "px"; | |
} | |
}, | |
addCreationNode: function(hasChildren) | |
{ | |
if (this.creationNode) | |
this.creationNode.makeNormal(); | |
var emptyData = {}; | |
for (var column in this.columns) | |
emptyData[column] = ''; | |
this.creationNode = new WebInspector.CreationDataGridNode(emptyData, hasChildren); | |
this.appendChild(this.creationNode); | |
}, | |
appendChild: function(child) | |
{ | |
this.insertChild(child, this.children.length); | |
}, | |
insertChild: function(child, index) | |
{ | |
if (!child) | |
throw("insertChild: Node can't be undefined or null."); | |
if (child.parent === this) | |
throw("insertChild: Node is already a child of this node."); | |
if (child.parent) | |
child.parent.removeChild(child); | |
this.children.splice(index, 0, child); | |
this.hasChildren = true; | |
child.parent = this; | |
child.dataGrid = this.dataGrid; | |
child._recalculateSiblings(index); | |
delete child._depth; | |
delete child._revealed; | |
delete child._attached; | |
child._shouldRefreshChildren = true; | |
var current = child.children[0]; | |
while (current) { | |
current.dataGrid = this.dataGrid; | |
delete current._depth; | |
delete current._revealed; | |
delete current._attached; | |
current._shouldRefreshChildren = true; | |
current = current.traverseNextNode(false, child, true); | |
} | |
if (this.expanded) | |
child._attach(); | |
}, | |
removeChild: function(child) | |
{ | |
if (!child) | |
throw("removeChild: Node can't be undefined or null."); | |
if (child.parent !== this) | |
throw("removeChild: Node is not a child of this node."); | |
child.deselect(); | |
this.children.remove(child, true); | |
if (child.previousSibling) | |
child.previousSibling.nextSibling = child.nextSibling; | |
if (child.nextSibling) | |
child.nextSibling.previousSibling = child.previousSibling; | |
child.dataGrid = null; | |
child.parent = null; | |
child.nextSibling = null; | |
child.previousSibling = null; | |
if (this.children.length <= 0) | |
this.hasChildren = false; | |
}, | |
removeChildren: function() | |
{ | |
for (var i = 0; i < this.children.length; ++i) { | |
var child = this.children[i]; | |
child.deselect(); | |
child._detach(); | |
child.dataGrid = null; | |
child.parent = null; | |
child.nextSibling = null; | |
child.previousSibling = null; | |
} | |
this.children = []; | |
this.hasChildren = false; | |
}, | |
removeChildrenRecursive: function() | |
{ | |
var childrenToRemove = this.children; | |
var child = this.children[0]; | |
while (child) { | |
if (child.children.length) | |
childrenToRemove = childrenToRemove.concat(child.children); | |
child = child.traverseNextNode(false, this, true); | |
} | |
for (var i = 0; i < childrenToRemove.length; ++i) { | |
var child = childrenToRemove[i]; | |
child.deselect(); | |
child._detach(); | |
child.children = []; | |
child.dataGrid = null; | |
child.parent = null; | |
child.nextSibling = null; | |
child.previousSibling = null; | |
} | |
this.children = []; | |
}, | |
_keyDown: function(event) | |
{ | |
if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing) | |
return; | |
var handled = false; | |
var nextSelectedNode; | |
if (event.keyIdentifier === "Up" && !event.altKey) { | |
nextSelectedNode = this.selectedNode.traversePreviousNode(true); | |
while (nextSelectedNode && !nextSelectedNode.selectable) | |
nextSelectedNode = nextSelectedNode.traversePreviousNode(!this.expandTreeNodesWhenArrowing); | |
handled = nextSelectedNode ? true : false; | |
} else if (event.keyIdentifier === "Down" && !event.altKey) { | |
nextSelectedNode = this.selectedNode.traverseNextNode(true); | |
while (nextSelectedNode && !nextSelectedNode.selectable) | |
nextSelectedNode = nextSelectedNode.traverseNextNode(!this.expandTreeNodesWhenArrowing); | |
handled = nextSelectedNode ? true : false; | |
} else if (event.keyIdentifier === "Left") { | |
if (this.selectedNode.expanded) { | |
if (event.altKey) | |
this.selectedNode.collapseRecursively(); | |
else | |
this.selectedNode.collapse(); | |
handled = true; | |
} else if (this.selectedNode.parent && !this.selectedNode.parent.root) { | |
handled = true; | |
if (this.selectedNode.parent.selectable) { | |
nextSelectedNode = this.selectedNode.parent; | |
handled = nextSelectedNode ? true : false; | |
} else if (this.selectedNode.parent) | |
this.selectedNode.parent.collapse(); | |
} | |
} else if (event.keyIdentifier === "Right") { | |
if (!this.selectedNode.revealed) { | |
this.selectedNode.reveal(); | |
handled = true; | |
} else if (this.selectedNode.hasChildren) { | |
handled = true; | |
if (this.selectedNode.expanded) { | |
nextSelectedNode = this.selectedNode.children[0]; | |
handled = nextSelectedNode ? true : false; | |
} else { | |
if (event.altKey) | |
this.selectedNode.expandRecursively(); | |
else | |
this.selectedNode.expand(); | |
} | |
} | |
} else if (event.keyCode === 8 || event.keyCode === 46) { | |
if (this._deleteCallback) { | |
handled = true; | |
this._deleteCallback(this.selectedNode); | |
} | |
} else if (isEnterKey(event)) { | |
if (this._editCallback) { | |
handled = true; | |
// The first child of the selected element is the <td class="0-column">, | |
// and that's what we want to edit. | |
this._startEditing(this.selectedNode._element.children[0]); | |
} | |
} | |
if (nextSelectedNode) { | |
nextSelectedNode.reveal(); | |
nextSelectedNode.select(); | |
} | |
if (handled) { | |
event.preventDefault(); | |
event.stopPropagation(); | |
} | |
}, | |
expand: function() | |
{ | |
// This is the root, do nothing. | |
}, | |
collapse: function() | |
{ | |
// This is the root, do nothing. | |
}, | |
reveal: function() | |
{ | |
// This is the root, do nothing. | |
}, | |
dataGridNodeFromNode: function(target) | |
{ | |
var rowElement = target.enclosingNodeOrSelfWithNodeName("tr"); | |
return rowElement._dataGridNode; | |
}, | |
dataGridNodeFromPoint: function(x, y) | |
{ | |
var node = this._dataTable.ownerDocument.elementFromPoint(x, y); | |
var rowElement = node.enclosingNodeOrSelfWithNodeName("tr"); | |
return rowElement._dataGridNode; | |
}, | |
_clickInHeaderCell: function(event) | |
{ | |
var cell = event.target.enclosingNodeOrSelfWithNodeName("th"); | |
if (!cell || !cell.columnIdentifier || !cell.hasStyleClass("sortable")) | |
return; | |
var sortOrder = this.sortOrder; | |
if (this._sortColumnCell) { | |
this._sortColumnCell.removeStyleClass("sort-ascending"); | |
this._sortColumnCell.removeStyleClass("sort-descending"); | |
} | |
if (cell == this._sortColumnCell) { | |
if (sortOrder == "ascending") | |
sortOrder = "descending"; | |
else | |
sortOrder = "ascending"; | |
} | |
this._sortColumnCell = cell; | |
cell.addStyleClass("sort-" + sortOrder); | |
this.dispatchEventToListeners("sorting changed"); | |
}, | |
_mouseDownInDataTable: function(event) | |
{ | |
var gridNode = this.dataGridNodeFromNode(event.target); | |
if (!gridNode || !gridNode.selectable) | |
return; | |
if (gridNode.isEventWithinDisclosureTriangle(event)) | |
return; | |
if (event.metaKey) { | |
if (gridNode.selected) | |
gridNode.deselect(); | |
else | |
gridNode.select(); | |
} else | |
gridNode.select(); | |
}, | |
_contextMenuInDataTable: function(event) | |
{ | |
var gridNode = this.dataGridNodeFromNode(event.target); | |
if (!gridNode || !gridNode.selectable) | |
return; | |
if (gridNode.isEventWithinDisclosureTriangle(event)) | |
return; | |
var contextMenu = new WebInspector.ContextMenu(); | |
// FIXME: Use the column names for Editing, instead of just "Edit". | |
if (this.dataGrid._editCallback) { | |
if (gridNode === this.creationNode) | |
contextMenu.appendItem(WebInspector.UIString("Add New"), this._startEditing.bind(this, event.target)); | |
else | |
contextMenu.appendItem(WebInspector.UIString("Edit"), this._startEditing.bind(this, event.target)); | |
} | |
if (this.dataGrid._deleteCallback && gridNode !== this.creationNode) | |
contextMenu.appendItem(WebInspector.UIString("Delete"), this._deleteCallback.bind(this, gridNode)); | |
contextMenu.show(event); | |
}, | |
_clickInDataTable: function(event) | |
{ | |
var gridNode = this.dataGridNodeFromNode(event.target); | |
if (!gridNode || !gridNode.hasChildren) | |
return; | |
if (!gridNode.isEventWithinDisclosureTriangle(event)) | |
return; | |
if (gridNode.expanded) { | |
if (event.altKey) | |
gridNode.collapseRecursively(); | |
else | |
gridNode.collapse(); | |
} else { | |
if (event.altKey) | |
gridNode.expandRecursively(); | |
else | |
gridNode.expand(); | |
} | |
}, | |
_startResizerDragging: function(event) | |
{ | |
this.currentResizer = event.target; | |
if (!this.currentResizer.rightNeighboringColumnID) | |
return; | |
WebInspector.elementDragStart(this.lastResizer, this._resizerDragging.bind(this), | |
this._endResizerDragging.bind(this), event, "col-resize"); | |
}, | |
_resizerDragging: function(event) | |
{ | |
var resizer = this.currentResizer; | |
if (!resizer) | |
return; | |
// Constrain the dragpoint to be within the containing div of the | |
// datagrid. | |
var dragPoint = event.clientX - this.element.totalOffsetLeft; | |
// Constrain the dragpoint to be within the space made up by the | |
// column directly to the left and the column directly to the right. | |
var leftEdgeOfPreviousColumn = 0; | |
var firstRowCells = this.headerTableBody.rows[0].cells; | |
for (var i = 0; i < resizer.rightNeighboringColumnID - 1; i++) | |
leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth; | |
var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[resizer.rightNeighboringColumnID - 1].offsetWidth + firstRowCells[resizer.rightNeighboringColumnID].offsetWidth; | |
// Give each column some padding so that they don't disappear. | |
var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding; | |
var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding; | |
dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum); | |
resizer.style.left = (dragPoint - this.CenterResizerOverBorderAdjustment) + "px"; | |
var percentLeftColumn = (((dragPoint - leftEdgeOfPreviousColumn) / this._dataTable.offsetWidth) * 100) + "%"; | |
this._headerTableColumnGroup.children[resizer.rightNeighboringColumnID - 1].style.width = percentLeftColumn; | |
this._dataTableColumnGroup.children[resizer.rightNeighboringColumnID - 1].style.width = percentLeftColumn; | |
var percentRightColumn = (((rightEdgeOfNextColumn - dragPoint) / this._dataTable.offsetWidth) * 100) + "%"; | |
this._headerTableColumnGroup.children[resizer.rightNeighboringColumnID].style.width = percentRightColumn; | |
this._dataTableColumnGroup.children[resizer.rightNeighboringColumnID].style.width = percentRightColumn; | |
event.preventDefault(); | |
}, | |
_endResizerDragging: function(event) | |
{ | |
WebInspector.elementDragEnd(event); | |
this.currentResizer = null; | |
}, | |
ColumnResizePadding: 10, | |
CenterResizerOverBorderAdjustment: 3, | |
} | |
WebInspector.DataGrid.prototype.__proto__ = WebInspector.Object.prototype; | |
WebInspector.DataGridNode = function(data, hasChildren) | |
{ | |
this._expanded = false; | |
this._selected = false; | |
this._shouldRefreshChildren = true; | |
this._data = data || {}; | |
this.hasChildren = hasChildren || false; | |
this.children = []; | |
this.dataGrid = null; | |
this.parent = null; | |
this.previousSibling = null; | |
this.nextSibling = null; | |
this.disclosureToggleWidth = 10; | |
} | |
WebInspector.DataGridNode.prototype = { | |
selectable: true, | |
get element() | |
{ | |
if (this._element) | |
return this._element; | |
if (!this.dataGrid) | |
return null; | |
this._element = document.createElement("tr"); | |
this._element._dataGridNode = this; | |
if (this.hasChildren) | |
this._element.addStyleClass("parent"); | |
if (this.expanded) | |
this._element.addStyleClass("expanded"); | |
if (this.selected) | |
this._element.addStyleClass("selected"); | |
if (this.revealed) | |
this._element.addStyleClass("revealed"); | |
for (var columnIdentifier in this.dataGrid.columns) { | |
var cell = this.createCell(columnIdentifier); | |
this._element.appendChild(cell); | |
} | |
return this._element; | |
}, | |
get data() | |
{ | |
return this._data; | |
}, | |
set data(x) | |
{ | |
this._data = x || {}; | |
this.refresh(); | |
}, | |
get revealed() | |
{ | |
if ("_revealed" in this) | |
return this._revealed; | |
var currentAncestor = this.parent; | |
while (currentAncestor && !currentAncestor.root) { | |
if (!currentAncestor.expanded) { | |
this._revealed = false; | |
return false; | |
} | |
currentAncestor = currentAncestor.parent; | |
} | |
this._revealed = true; | |
return true; | |
}, | |
set hasChildren(x) | |
{ | |
if (this._hasChildren === x) | |
return; | |
this._hasChildren = x; | |
if (!this._element) | |
return; | |
if (this._hasChildren) | |
{ | |
this._element.addStyleClass("parent"); | |
if (this.expanded) | |
this._element.addStyleClass("expanded"); | |
} | |
else | |
{ | |
this._element.removeStyleClass("parent"); | |
this._element.removeStyleClass("expanded"); | |
} | |
}, | |
get hasChildren() | |
{ | |
return this._hasChildren; | |
}, | |
set revealed(x) | |
{ | |
if (this._revealed === x) | |
return; | |
this._revealed = x; | |
if (this._element) { | |
if (this._revealed) | |
this._element.addStyleClass("revealed"); | |
else | |
this._element.removeStyleClass("revealed"); | |
} | |
for (var i = 0; i < this.children.length; ++i) | |
this.children[i].revealed = x && this.expanded; | |
}, | |
get depth() | |
{ | |
if ("_depth" in this) | |
return this._depth; | |
if (this.parent && !this.parent.root) | |
this._depth = this.parent.depth + 1; | |
else | |
this._depth = 0; | |
return this._depth; | |
}, | |
get shouldRefreshChildren() | |
{ | |
return this._shouldRefreshChildren; | |
}, | |
set shouldRefreshChildren(x) | |
{ | |
this._shouldRefreshChildren = x; | |
if (x && this.expanded) | |
this.expand(); | |
}, | |
get selected() | |
{ | |
return this._selected; | |
}, | |
set selected(x) | |
{ | |
if (x) | |
this.select(); | |
else | |
this.deselect(); | |
}, | |
get expanded() | |
{ | |
return this._expanded; | |
}, | |
set expanded(x) | |
{ | |
if (x) | |
this.expand(); | |
else | |
this.collapse(); | |
}, | |
refresh: function() | |
{ | |
if (!this._element || !this.dataGrid) | |
return; | |
this._element.removeChildren(); | |
for (var columnIdentifier in this.dataGrid.columns) { | |
var cell = this.createCell(columnIdentifier); | |
this._element.appendChild(cell); | |
} | |
}, | |
createCell: function(columnIdentifier) | |
{ | |
var cell = document.createElement("td"); | |
cell.className = columnIdentifier + "-column"; | |
var alignment = this.dataGrid.aligned[columnIdentifier]; | |
if (alignment) | |
cell.addStyleClass(alignment); | |
var div = document.createElement("div"); | |
div.textContent = this.data[columnIdentifier]; | |
cell.appendChild(div); | |
if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) { | |
cell.addStyleClass("disclosure"); | |
if (this.depth) | |
cell.style.setProperty("padding-left", (this.depth * this.dataGrid.indentWidth) + "px"); | |
} | |
return cell; | |
}, | |
// Share these functions with DataGrid. They are written to work with a DataGridNode this object. | |
appendChild: WebInspector.DataGrid.prototype.appendChild, | |
insertChild: WebInspector.DataGrid.prototype.insertChild, | |
removeChild: WebInspector.DataGrid.prototype.removeChild, | |
removeChildren: WebInspector.DataGrid.prototype.removeChildren, | |
removeChildrenRecursive: WebInspector.DataGrid.prototype.removeChildrenRecursive, | |
_recalculateSiblings: function(myIndex) | |
{ | |
if (!this.parent) | |
return; | |
var previousChild = (myIndex > 0 ? this.parent.children[myIndex - 1] : null); | |
if (previousChild) { | |
previousChild.nextSibling = this; | |
this.previousSibling = previousChild; | |
} else | |
this.previousSibling = null; | |
var nextChild = this.parent.children[myIndex + 1]; | |
if (nextChild) { | |
nextChild.previousSibling = this; | |
this.nextSibling = nextChild; | |
} else | |
this.nextSibling = null; | |
}, | |
collapse: function() | |
{ | |
if (this._element) | |
this._element.removeStyleClass("expanded"); | |
this._expanded = false; | |
for (var i = 0; i < this.children.length; ++i) | |
this.children[i].revealed = false; | |
this.dispatchEventToListeners("collapsed"); | |
}, | |
collapseRecursively: function() | |
{ | |
var item = this; | |
while (item) { | |
if (item.expanded) | |
item.collapse(); | |
item = item.traverseNextNode(false, this, true); | |
} | |
}, | |
expand: function() | |
{ | |
if (!this.hasChildren || this.expanded) | |
return; | |
if (this.revealed && !this._shouldRefreshChildren) | |
for (var i = 0; i < this.children.length; ++i) | |
this.children[i].revealed = true; | |
if (this._shouldRefreshChildren) { | |
for (var i = 0; i < this.children.length; ++i) | |
this.children[i]._detach(); | |
this.dispatchEventToListeners("populate"); | |
if (this._attached) { | |
for (var i = 0; i < this.children.length; ++i) { | |
var child = this.children[i]; | |
if (this.revealed) | |
child.revealed = true; | |
child._attach(); | |
} | |
} | |
delete this._shouldRefreshChildren; | |
} | |
if (this._element) | |
this._element.addStyleClass("expanded"); | |
this._expanded = true; | |
this.dispatchEventToListeners("expanded"); | |
}, | |
expandRecursively: function() | |
{ | |
var item = this; | |
while (item) { | |
item.expand(); | |
item = item.traverseNextNode(false, this); | |
} | |
}, | |
reveal: function() | |
{ | |
var currentAncestor = this.parent; | |
while (currentAncestor && !currentAncestor.root) { | |
if (!currentAncestor.expanded) | |
currentAncestor.expand(); | |
currentAncestor = currentAncestor.parent; | |
} | |
this.element.scrollIntoViewIfNeeded(false); | |
this.dispatchEventToListeners("revealed"); | |
}, | |
select: function(supressSelectedEvent) | |
{ | |
if (!this.dataGrid || !this.selectable || this.selected) | |
return; | |
if (this.dataGrid.selectedNode) | |
this.dataGrid.selectedNode.deselect(); | |
this._selected = true; | |
this.dataGrid.selectedNode = this; | |
if (this._element) | |
this._element.addStyleClass("selected"); | |
if (!supressSelectedEvent) | |
this.dispatchEventToListeners("selected"); | |
}, | |
deselect: function(supressDeselectedEvent) | |
{ | |
if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected) | |
return; | |
this._selected = false; | |
this.dataGrid.selectedNode = null; | |
if (this._element) | |
this._element.removeStyleClass("selected"); | |
if (!supressDeselectedEvent) | |
this.dispatchEventToListeners("deselected"); | |
}, | |
traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info) | |
{ | |
if (!dontPopulate && this.hasChildren) | |
this.dispatchEventToListeners("populate"); | |
if (info) | |
info.depthChange = 0; | |
var node = (!skipHidden || this.revealed) ? this.children[0] : null; | |
if (node && (!skipHidden || this.expanded)) { | |
if (info) | |
info.depthChange = 1; | |
return node; | |
} | |
if (this === stayWithin) | |
return null; | |
node = (!skipHidden || this.revealed) ? this.nextSibling : null; | |
if (node) | |
return node; | |
node = this; | |
while (node && !node.root && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) { | |
if (info) | |
info.depthChange -= 1; | |
node = node.parent; | |
} | |
if (!node) | |
return null; | |
return (!skipHidden || node.revealed) ? node.nextSibling : null; | |
}, | |
traversePreviousNode: function(skipHidden, dontPopulate) | |
{ | |
var node = (!skipHidden || this.revealed) ? this.previousSibling : null; | |
if (!dontPopulate && node && node.hasChildren) | |
node.dispatchEventToListeners("populate"); | |
while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null)) { | |
if (!dontPopulate && node.hasChildren) | |
node.dispatchEventToListeners("populate"); | |
node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null); | |
} | |
if (node) | |
return node; | |
if (!this.parent || this.parent.root) | |
return null; | |
return this.parent; | |
}, | |
isEventWithinDisclosureTriangle: function(event) | |
{ | |
if (!this.hasChildren) | |
return false; | |
var cell = event.target.enclosingNodeOrSelfWithNodeName("td"); | |
if (!cell.hasStyleClass("disclosure")) | |
return false; | |
var computedLeftPadding = window.getComputedStyle(cell).getPropertyCSSValue("padding-left").getFloatValue(CSSPrimitiveValue.CSS_PX); | |
var left = cell.totalOffsetLeft + computedLeftPadding; | |
return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth; | |
}, | |
_attach: function() | |
{ | |
if (!this.dataGrid || this._attached) | |
return; | |
this._attached = true; | |
var nextNode = null; | |
var previousNode = this.traversePreviousNode(true, true); | |
if (previousNode && previousNode.element.parentNode && previousNode.element.nextSibling) | |
var nextNode = previousNode.element.nextSibling; | |
if (!nextNode) | |
nextNode = this.dataGrid.dataTableBody.lastChild; | |
this.dataGrid.dataTableBody.insertBefore(this.element, nextNode); | |
if (this.expanded) | |
for (var i = 0; i < this.children.length; ++i) | |
this.children[i]._attach(); | |
}, | |
_detach: function() | |
{ | |
if (!this._attached) | |
return; | |
this._attached = false; | |
if (this._element && this._element.parentNode) | |
this._element.parentNode.removeChild(this._element); | |
for (var i = 0; i < this.children.length; ++i) | |
this.children[i]._detach(); | |
}, | |
savePosition: function() | |
{ | |
if (this._savedPosition) | |
return; | |
if (!this.parent) | |
throw("savePosition: Node must have a parent."); | |
this._savedPosition = { | |
parent: this.parent, | |
index: this.parent.children.indexOf(this) | |
}; | |
}, | |
restorePosition: function() | |
{ | |
if (!this._savedPosition) | |
return; | |
if (this.parent !== this._savedPosition.parent) | |
this._savedPosition.parent.insertChild(this, this._savedPosition.index); | |
delete this._savedPosition; | |
} | |
} | |
WebInspector.DataGridNode.prototype.__proto__ = WebInspector.Object.prototype; | |
WebInspector.CreationDataGridNode = function(data, hasChildren) | |
{ | |
WebInspector.DataGridNode.call(this, data, hasChildren); | |
this.isCreationNode = true; | |
} | |
WebInspector.CreationDataGridNode.prototype = { | |
makeNormal: function() | |
{ | |
delete this.isCreationNode; | |
delete this.makeNormal; | |
} | |
} | |
WebInspector.CreationDataGridNode.prototype.__proto__ = WebInspector.DataGridNode.prototype; |