blob: 02b70242baaa40d59a3a0c64302aa6b17843c626 [file] [log] [blame]
/*
* Copyright (C) 2008, 2013-2017 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. AND ITS CONTRIBUTORS ``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 ITS 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.
*/
WI.DataGridNode = class DataGridNode extends WI.Object
{
constructor(data, hasChildren, classNames)
{
super();
this._expanded = false;
this._hidden = false;
this._selected = false;
this._copyable = true;
this._editable = true;
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;
this._classNames = classNames || [];
}
get hidden()
{
return this._hidden;
}
set hidden(x)
{
x = !!x;
if (this._hidden === x)
return;
this._hidden = x;
if (this._element)
this._element.classList.toggle("hidden", this._hidden);
if (this.dataGrid)
this.dataGrid._noteRowsChanged();
}
get selectable()
{
return this._element && !this._hidden;
}
get copyable()
{
return this._copyable;
}
set copyable(x)
{
this._copyable = x;
}
get editable()
{
return this._editable;
}
set editable(x)
{
this._editable = x;
}
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.classList.add("parent");
if (this.expanded)
this._element.classList.add("expanded");
if (this.selected)
this._element.classList.add("selected");
if (this.revealed)
this._element.classList.add("revealed");
if (this._hidden)
this._element.classList.add("hidden");
this._element.classList.add(...this._classNames);
this.createCells();
return this._element;
}
createCells()
{
for (var columnIdentifier of this.dataGrid.orderedColumns)
this._element.appendChild(this.createCell(columnIdentifier));
}
refreshIfNeeded()
{
if (!this._needsRefresh)
return;
this._needsRefresh = false;
this.refresh();
}
needsRefresh()
{
this._needsRefresh = true;
if (!this._revealed)
return;
if (this._scheduledRefreshIdentifier)
return;
this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
}
get data()
{
return this._data;
}
set data(x)
{
console.assert(typeof x === "object", "Data should be an object.");
x = x || {};
if (Object.shallowEqual(this._data, x))
return;
this._data = x;
this.needsRefresh();
}
get filterableData()
{
if (this._cachedFilterableData)
return this._cachedFilterableData;
this._cachedFilterableData = [];
for (let column of this.dataGrid.columns.values()) {
if (column.hidden)
continue;
let value = this.filterableDataForColumn(column.columnIdentifier);
if (!value)
continue;
if (!(value instanceof Array))
value = [value];
if (!value.length)
continue;
this._cachedFilterableData.pushAll(value);
}
return this._cachedFilterableData;
}
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.classList.add("parent");
if (this.expanded)
this._element.classList.add("expanded");
} else
this._element.classList.remove("parent", "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.classList.add("revealed");
else
this._element.classList.remove("revealed");
}
this.refreshIfNeeded();
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 indentPadding()
{
if (typeof this._indentPadding === "number")
return this._indentPadding;
this._indentPadding = this.depth * this.dataGrid.indentWidth;
return this._indentPadding;
}
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();
}
hasAncestor(ancestor)
{
if (!ancestor)
return false;
let currentAncestor = this.parent;
while (currentAncestor) {
if (ancestor === currentAncestor)
return true;
currentAncestor = currentAncestor.parent;
}
return false;
}
refresh()
{
if (!this._element || !this.dataGrid)
return;
if (this._scheduledRefreshIdentifier) {
cancelAnimationFrame(this._scheduledRefreshIdentifier);
this._scheduledRefreshIdentifier = undefined;
}
this._cachedFilterableData = null;
this._needsRefresh = false;
this._element.removeChildren();
this.createCells();
}
refreshRecursively()
{
this.refresh();
this.forEachChildInSubtree((node) => node.refresh());
}
updateLayout()
{
// Implemented by subclasses if needed.
}
createCell(columnIdentifier)
{
var cellElement = document.createElement("td");
cellElement.className = columnIdentifier + "-column";
cellElement.__columnIdentifier = columnIdentifier;
var div = cellElement.createChild("div", "cell-content");
var content = this.createCellContent(columnIdentifier, cellElement);
div.append(content);
let column = this.dataGrid.columns.get(columnIdentifier);
if (column) {
if (column["aligned"])
cellElement.classList.add(column["aligned"]);
if (column["group"])
cellElement.classList.add("column-group-" + column["group"]);
if (column["icon"]) {
let iconElement = document.createElement("div");
iconElement.classList.add("icon");
div.insertBefore(iconElement, div.firstChild);
}
}
if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
cellElement.classList.add("disclosure");
if (this.indentPadding) {
if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
cellElement.style.setProperty("padding-right", `${this.indentPadding}px`);
else
cellElement.style.setProperty("padding-left", `${this.indentPadding}px`);
}
}
return cellElement;
}
createCellContent(columnIdentifier)
{
if (!(columnIdentifier in this.data))
return zeroWidthSpace; // Zero width space to keep the cell from collapsing.
let data = this.data[columnIdentifier];
return typeof data === "number" ? data.maxDecimals(2).toLocaleString() : data;
}
elementWithColumnIdentifier(columnIdentifier)
{
if (!this.dataGrid)
return null;
let index = this.dataGrid.orderedColumns.indexOf(columnIdentifier);
if (index === -1)
return null;
return this.element.children[index];
}
// Share these functions with DataGrid. They are written to work with a DataGridNode this object.
appendChild() { return WI.DataGrid.prototype.appendChild.apply(this, arguments); }
insertChild() { return WI.DataGrid.prototype.insertChild.apply(this, arguments); }
removeChild() { return WI.DataGrid.prototype.removeChild.apply(this, arguments); }
removeChildren() { return WI.DataGrid.prototype.removeChildren.apply(this, arguments); }
_recalculateSiblings(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()
{
if (this._element)
this._element.classList.remove("expanded");
this._expanded = false;
for (var i = 0; i < this.children.length; ++i)
this.children[i].revealed = false;
this.dispatchEventToListeners("collapsed");
if (this.dataGrid) {
this.dataGrid.dispatchEventToListeners(WI.DataGrid.Event.CollapsedNode, {dataGridNode: this});
this.dataGrid._noteRowsChanged();
}
}
collapseRecursively()
{
var item = this;
while (item) {
if (item.expanded)
item.collapse();
item = item.traverseNextNode(false, this, true);
}
}
expand()
{
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();
}
}
this._shouldRefreshChildren = false;
}
if (this._element)
this._element.classList.add("expanded");
this._expanded = true;
this.dispatchEventToListeners("expanded");
if (this.dataGrid) {
this.dataGrid.dispatchEventToListeners(WI.DataGrid.Event.ExpandedNode, {dataGridNode: this});
this.dataGrid._noteRowsChanged();
}
}
expandRecursively()
{
var item = this;
while (item) {
item.expand();
item = item.traverseNextNode(false, this);
}
}
forEachImmediateChild(callback)
{
for (let node of this.children)
callback(node);
}
forEachChildInSubtree(callback)
{
let node = this.traverseNextNode(false, this, true);
while (node) {
callback(node);
node = node.traverseNextNode(false, this, true);
}
}
isInSubtreeOfNode(baseNode)
{
let node = baseNode;
while (node) {
if (node === this)
return true;
node = node.traverseNextNode(false, baseNode, true);
}
return false;
}
reveal()
{
var currentAncestor = this.parent;
while (currentAncestor && !currentAncestor.root) {
if (!currentAncestor.expanded)
currentAncestor.expand();
currentAncestor = currentAncestor.parent;
}
this.dataGrid.updateVisibleRows(this);
this.dispatchEventToListeners("revealed");
}
select(suppressSelectedEvent)
{
if (!this.dataGrid || !this.selectable || this.selected)
return;
let oldSelectedNode = this.dataGrid.selectedNode;
if (oldSelectedNode)
oldSelectedNode.deselect(true);
this._selected = true;
this.dataGrid.selectedNode = this;
if (this._element)
this._element.classList.add("selected");
if (!suppressSelectedEvent)
this.dataGrid.dispatchEventToListeners(WI.DataGrid.Event.SelectedNodeChanged, {oldSelectedNode});
}
revealAndSelect(suppressSelectedEvent)
{
this.reveal();
this.select(suppressSelectedEvent);
}
deselect(suppressDeselectedEvent)
{
if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
return;
this._selected = false;
this.dataGrid.selectedNode = null;
if (this._element)
this._element.classList.remove("selected");
if (!suppressDeselectedEvent)
this.dataGrid.dispatchEventToListeners(WI.DataGrid.Event.SelectedNodeChanged, {oldSelectedNode: this});
}
traverseNextNode(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(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.lastValue : null)) {
if (!dontPopulate && node.hasChildren)
node.dispatchEventToListeners("populate");
node = (!skipHidden || (node.revealed && node.expanded)) ? node.children.lastValue : null;
}
if (node)
return node;
if (!this.parent || this.parent.root)
return null;
return this.parent;
}
isEventWithinDisclosureTriangle(event)
{
if (!this.hasChildren)
return false;
let cell = event.target.closest("td");
if (!cell || !cell.classList.contains("disclosure"))
return false;
let computedStyle = window.getComputedStyle(cell);
let start = 0;
if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
start += cell.totalOffsetRight - computedStyle.getPropertyCSSValue("padding-right").getFloatValue(CSSPrimitiveValue.CSS_PX) - this.disclosureToggleWidth;
else
start += cell.totalOffsetLeft + computedStyle.getPropertyCSSValue("padding-left").getFloatValue(CSSPrimitiveValue.CSS_PX);
return event.pageX >= start && event.pageX <= start + this.disclosureToggleWidth;
}
_attach()
{
if (!this.dataGrid || this._attached)
return;
this._attached = true;
let insertionIndex = -1;
if (!this.isPlaceholderNode) {
var previousGridNode = this.traversePreviousNode(true, true);
insertionIndex = this.dataGrid._rows.indexOf(previousGridNode);
if (insertionIndex === -1)
insertionIndex = 0;
else
insertionIndex++;
}
if (insertionIndex === -1)
this.dataGrid._rows.push(this);
else
this.dataGrid._rows.insertAtIndex(this, insertionIndex);
this.dataGrid._noteRowsChanged();
if (this.expanded) {
for (var i = 0; i < this.children.length; ++i)
this.children[i]._attach();
}
}
_detach()
{
if (!this._attached)
return;
this._attached = false;
this.dataGrid._rows.remove(this, true);
this.dataGrid._noteRowRemoved(this);
for (var i = 0; i < this.children.length; ++i)
this.children[i]._detach();
}
savePosition()
{
if (this._savedPosition)
return;
console.assert(this.parent);
if (!this.parent)
return;
this._savedPosition = {
parent: this.parent,
index: this.parent.children.indexOf(this)
};
}
restorePosition()
{
if (!this._savedPosition)
return;
if (this.parent !== this._savedPosition.parent)
this._savedPosition.parent.insertChild(this, this._savedPosition.index);
this._savedPosition = null;
}
appendContextMenuItems(contextMenu)
{
// Subclasses may override
return null;
}
// Protected
filterableDataForColumn(columnIdentifier)
{
let value = this.data[columnIdentifier];
return typeof value === "string" ? value : null;
}
didResizeColumn(columnIdentifier)
{
// Override by subclasses.
}
};
// Used to create a new table row when entering new data by editing cells.
WI.PlaceholderDataGridNode = class PlaceholderDataGridNode extends WI.DataGridNode
{
constructor(data)
{
super(data, false);
this.isPlaceholderNode = true;
}
makeNormal()
{
this.isPlaceholderNode = false;
}
};