blob: ac56bf20a7e6f754d203149657c519c247d2e64e [file] [log] [blame]
/*
* Copyright (C) 2007, 2013, 2015 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.
* 3. Neither the name of Apple Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE 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 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.TreeElement = class TreeElement extends WI.Object
{
constructor(title, representedObject, options = {})
{
super();
this._title = title;
this.representedObject = representedObject || {};
if (this.representedObject.__treeElementIdentifier)
this.identifier = this.representedObject.__treeElementIdentifier;
else {
this.identifier = WI.TreeOutline._knownTreeElementNextIdentifier++;
this.representedObject.__treeElementIdentifier = this.identifier;
}
this._hidden = false;
this._selectable = true;
this.expanded = false;
this.selected = false;
this.hasChildren = options.hasChildren;
this.children = [];
this.treeOutline = null;
this.parent = null;
this.previousSibling = null;
this.nextSibling = null;
this._listItemNode = null;
}
// Methods
appendChild() { return WI.TreeOutline.prototype.appendChild.apply(this, arguments); }
insertChild() { return WI.TreeOutline.prototype.insertChild.apply(this, arguments); }
removeChild() { return WI.TreeOutline.prototype.removeChild.apply(this, arguments); }
removeChildAtIndex() { return WI.TreeOutline.prototype.removeChildAtIndex.apply(this, arguments); }
removeChildren() { return WI.TreeOutline.prototype.removeChildren.apply(this, arguments); }
selfOrDescendant() { return WI.TreeOutline.prototype.selfOrDescendant.apply(this, arguments); }
get arrowToggleWidth()
{
return 10;
}
get selectable()
{
if (this._hidden)
return false;
return this._selectable;
}
set selectable(x)
{
this._selectable = x;
}
get listItemElement()
{
return this._listItemNode;
}
get title()
{
return this._title;
}
set title(x)
{
this._title = x;
this._setListItemNodeContent();
this.didChange();
}
get titleHTML()
{
return this._titleHTML;
}
set titleHTML(x)
{
this._titleHTML = x;
this._setListItemNodeContent();
this.didChange();
}
get tooltip()
{
return this._tooltip;
}
set tooltip(x)
{
this._tooltip = x;
if (this._listItemNode)
this._listItemNode.title = x ? x : "";
}
get hasChildren()
{
return this._hasChildren;
}
set hasChildren(x)
{
if (this._hasChildren === x)
return;
this._hasChildren = x;
if (!this._listItemNode)
return;
if (x)
this._listItemNode.classList.add("parent");
else {
this._listItemNode.classList.remove("parent");
this.collapse();
}
this.didChange();
}
get hidden()
{
return this._hidden;
}
set hidden(x)
{
if (this._hidden === x)
return;
this._hidden = x;
if (this._listItemNode)
this._listItemNode.hidden = this._hidden;
if (this._childrenListNode)
this._childrenListNode.hidden = this._hidden;
if (this.treeOutline) {
if (this.treeOutline.virtualized)
this.treeOutline.updateVirtualizedElementsDebouncer.delayForFrame();
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementVisibilityDidChange, {element: this});
}
}
get shouldRefreshChildren()
{
return this._shouldRefreshChildren;
}
set shouldRefreshChildren(x)
{
this._shouldRefreshChildren = x;
if (x && this.expanded)
this.expand();
}
get previousSelectableSibling()
{
let treeElement = this.previousSibling;
while (treeElement && !treeElement.selectable)
treeElement = treeElement.previousSibling;
return treeElement;
}
get nextSelectableSibling()
{
let treeElement = this.nextSibling;
while (treeElement && !treeElement.selectable)
treeElement = treeElement.nextSibling;
return treeElement;
}
canSelectOnMouseDown(event)
{
// Overridden by subclasses if needed.
return true;
}
_fireDidChange()
{
if (this.treeOutline)
this.treeOutline._treeElementDidChange(this);
}
didChange()
{
if (!this.treeOutline)
return;
if (!this._fireDidChangeDebouncer) {
this._fireDidChangeDebouncer = new Debouncer(() => {
this._fireDidChange();
});
}
this._fireDidChangeDebouncer.delayForFrame();
}
_setListItemNodeContent()
{
if (!this._listItemNode)
return;
if (!this._titleHTML && !this._title)
this._listItemNode.removeChildren();
else if (typeof this._titleHTML === "string")
this._listItemNode.innerHTML = this._titleHTML;
else if (typeof this._title === "string")
this._listItemNode.textContent = this._title;
else {
this._listItemNode.removeChildren();
if (this._title.parentNode)
this._title.parentNode.removeChild(this._title);
this._listItemNode.appendChild(this._title);
}
}
_attach()
{
if (!this._listItemNode || this.parent._shouldRefreshChildren) {
if (this.parent._shouldRefreshChildren)
this._detach();
this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
this._listItemNode.treeElement = this;
this._setListItemNodeContent();
this._listItemNode.title = this._tooltip ? this._tooltip : "";
this._listItemNode.hidden = this.hidden;
this._listItemNode.role = "treeitem";
if (this.hasChildren)
this._listItemNode.classList.add("parent");
if (this.expanded)
this._listItemNode.classList.add("expanded");
if (this.selected)
this._listItemNode.classList.add("selected");
this._listItemNode.addEventListener("click", WI.TreeElement.treeElementToggled);
this._listItemNode.addEventListener("dblclick", WI.TreeElement.treeElementDoubleClicked);
if (this.onattach)
this.onattach(this);
}
var nextSibling = null;
if (this.nextSibling && this.nextSibling._listItemNode && this.nextSibling._listItemNode.parentNode === this.parent._childrenListNode)
nextSibling = this.nextSibling._listItemNode;
if (!this.treeOutline || !this.treeOutline.virtualized) {
this.parent._childrenListNode.insertBefore(this._listItemNode, nextSibling);
if (this._childrenListNode)
this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
}
if (this.selected)
this.select();
if (this.expanded)
this.expand();
}
_detach()
{
if (this.ondetach && this._listItemNode)
this.ondetach(this);
if (this._listItemNode && this._listItemNode.parentNode)
this._listItemNode.parentNode.removeChild(this._listItemNode);
if (this._childrenListNode && this._childrenListNode.parentNode)
this._childrenListNode.parentNode.removeChild(this._childrenListNode);
}
static treeElementToggled(event)
{
let element = event.currentTarget;
if (!element)
return;
let treeElement = element.treeElement;
if (!treeElement)
return;
let toggleOnClick = treeElement.toggleOnClick && !treeElement.selectable;
if (toggleOnClick || treeElement.isEventWithinDisclosureTriangle(event)) {
if (treeElement.expanded) {
if (event.altKey)
treeElement.collapseRecursively();
else
treeElement.collapse();
} else {
if (event.altKey)
treeElement.expandRecursively();
else
treeElement.expand();
}
event.stopPropagation();
}
if (treeElement.treeOutline && !treeElement.treeOutline.selectable)
treeElement.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementClicked, {treeElement});
}
static treeElementDoubleClicked(event)
{
var element = event.currentTarget;
if (!element || !element.treeElement)
return;
if (element.treeElement.isEventWithinDisclosureTriangle(event))
return;
if (element.treeElement.dispatchEventToListeners(WI.TreeElement.Event.DoubleClick))
return;
if (element.treeElement.ondblclick)
element.treeElement.ondblclick.call(element.treeElement, event);
else if (element.treeElement.hasChildren && !element.treeElement.expanded)
element.treeElement.expand();
}
collapse()
{
if (this._listItemNode) {
this._listItemNode.classList.remove("expanded");
this._listItemNode.ariaExpanded = false;
}
if (this._childrenListNode) {
this._childrenListNode.classList.remove("expanded");
this._childrenListNode.ariaExpanded = false;
}
this.expanded = false;
if (this.treeOutline)
this.treeOutline._treeElementsExpandedState[this.identifier] = false;
if (this.oncollapse)
this.oncollapse(this);
if (this.treeOutline) {
if (this.treeOutline.virtualized)
this.treeOutline.updateVirtualizedElementsDebouncer.delayForFrame();
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementDisclosureDidChanged, {element: this});
}
}
collapseRecursively()
{
var item = this;
while (item) {
if (item.expanded)
item.collapse();
item = item.traverseNextTreeElement(false, this, true);
}
}
expand()
{
if (this.expanded && !this._shouldRefreshChildren && this._childrenListNode)
return;
// Set this before onpopulate. Since onpopulate can add elements and dispatch an ElementAdded event,
// this makes sure the expanded flag is true before calling those functions. This prevents the
// possibility of an infinite loop if onpopulate or an event handler were to call expand.
this.expanded = true;
if (this.treeOutline)
this.treeOutline._treeElementsExpandedState[this.identifier] = true;
// If there are no children, return. We will be expanded once we have children.
if (!this.hasChildren)
return;
if (this.treeOutline && (!this._childrenListNode || this._shouldRefreshChildren)) {
if (this._childrenListNode && this._childrenListNode.parentNode)
this._childrenListNode.parentNode.removeChild(this._childrenListNode);
this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
this._childrenListNode.parentTreeElement = this;
this._childrenListNode.classList.add("children");
this._childrenListNode.hidden = this.hidden;
this._childrenListNode.role = "group";
this.onpopulate();
// It is necessary to set expanded to true again here because some subclasses will call
// collapse in onpopulate (via removeChildren), which sets it back to false.
this.expanded = true;
for (var i = 0; i < this.children.length; ++i)
this.children[i]._attach();
this._shouldRefreshChildren = false;
}
if (this._listItemNode) {
this._listItemNode.classList.add("expanded");
this._listItemNode.ariaExpanded = true;
if (this._childrenListNode && this._childrenListNode.parentNode !== this._listItemNode.parentNode)
this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
}
if (this._childrenListNode) {
this._childrenListNode.classList.add("expanded");
this._childrenListNode.ariaExpanded = true;
}
if (this.onexpand)
this.onexpand(this);
if (this.treeOutline) {
if (this.treeOutline.virtualized)
this.treeOutline.updateVirtualizedElementsDebouncer.delayForFrame();
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementDisclosureDidChanged, {element: this});
}
}
expandRecursively(maxDepth)
{
var item = this;
var info = {};
var depth = 0;
// The Inspector uses TreeOutlines to represents object properties, so recursive expansion
// in some case can be infinite, since JavaScript objects can hold circular references.
// So default to a recursion cap of 3 levels, since that gives fairly good results.
if (maxDepth === undefined)
maxDepth = 3;
while (item) {
if (depth < maxDepth)
item.expand();
item = item.traverseNextTreeElement(false, this, depth >= maxDepth, info);
depth += info.depthChange;
}
}
hasAncestor(ancestor)
{
if (!ancestor)
return false;
var currentNode = this.parent;
while (currentNode) {
if (ancestor === currentNode)
return true;
currentNode = currentNode.parent;
}
return false;
}
reveal()
{
var currentAncestor = this.parent;
while (currentAncestor && !currentAncestor.root) {
if (!currentAncestor.expanded)
currentAncestor.expand();
currentAncestor = currentAncestor.parent;
}
// This must be called before onreveal, as some subclasses will scrollIntoViewIfNeeded and
// we should update the visible elements before attempting to scroll.
if (this.treeOutline && this.treeOutline.virtualized)
this.treeOutline.updateVirtualizedElementsDebouncer.force(this);
if (this.onreveal)
this.onreveal(this);
if (this.treeOutline)
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementRevealed, {element: this});
}
revealed(ignoreHidden)
{
if (!ignoreHidden && this.hidden)
return false;
var currentAncestor = this.parent;
while (currentAncestor && !currentAncestor.root) {
if (!currentAncestor.expanded)
return false;
if (!ignoreHidden && currentAncestor.hidden)
return false;
currentAncestor = currentAncestor.parent;
}
return true;
}
select(omitFocus, selectedByUser, suppressNotification)
{
let treeOutline = this.treeOutline;
if (!treeOutline || !this.selectable)
return;
if (!omitFocus)
this.focus();
else if (treeOutline.element.contains(document.activeElement)) {
// When treeOutline has focus, focus on the newly selected treeElement.
this.focus();
}
if (this.selected && !this.treeOutline.allowsRepeatSelection)
return;
// Focusing on another node may detach "this" from tree.
treeOutline = this.treeOutline;
if (!treeOutline)
return;
this.selected = true;
treeOutline.selectTreeElementInternal(this, suppressNotification, selectedByUser);
if (this._listItemNode)
this._listItemNode.ariaSelected = true;
}
revealAndSelect(omitFocus, selectedByUser, suppressNotification)
{
this.reveal();
this.select(omitFocus, selectedByUser, suppressNotification);
}
deselect(suppressNotification)
{
if (!this.treeOutline || !this.selected)
return false;
this.selected = false;
this.treeOutline.selectTreeElementInternal(null, suppressNotification);
if (this._listItemNode) {
this.unfocus();
this._listItemNode.ariaSelected = false;
}
return true;
}
focus()
{
if (!this._listItemNode)
return;
this._listItemNode.tabIndex = 0;
this._listItemNode.focus();
}
unfocus()
{
if (!this._listItemNode)
return;
this._listItemNode.removeAttribute("tabIndex");
}
onpopulate()
{
// Overridden by subclasses.
}
traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate, info)
{
function shouldSkip(element) {
return skipUnrevealed && !element.revealed(true);
}
var depthChange = 0;
var element = this;
if (!dontPopulate)
element.onpopulate();
do {
if (element.hasChildren && element.children[0] && (!skipUnrevealed || element.expanded)) {
element = element.children[0];
depthChange += 1;
} else {
while (element && !element.nextSibling && element.parent && !element.parent.root && element.parent !== stayWithin) {
element = element.parent;
depthChange -= 1;
}
if (element)
element = element.nextSibling;
}
} while (element && shouldSkip(element));
if (info)
info.depthChange = depthChange;
return element;
}
traversePreviousTreeElement(skipUnrevealed, dontPopulate)
{
function shouldSkip(element) {
return skipUnrevealed && !element.revealed(true);
}
var element = this;
do {
if (element.previousSibling) {
element = element.previousSibling;
while (element && element.hasChildren && element.expanded && !shouldSkip(element)) {
if (!dontPopulate)
element.onpopulate();
element = element.children.lastValue;
}
} else
element = element.parent && element.parent.root ? null : element.parent;
} while (element && shouldSkip(element));
return element;
}
isEventWithinDisclosureTriangle(event)
{
if (!document.contains(this._listItemNode))
return false;
// FIXME: We should not use getComputedStyle(). For that we need to get rid of using ::before for disclosure triangle. (http://webk.it/74446)
let computedStyle = window.getComputedStyle(this._listItemNode);
let start = 0;
if (computedStyle.direction === WI.LayoutDirection.RTL)
start += this._listItemNode.totalOffsetRight - computedStyle.getPropertyCSSValue("padding-right").getFloatValue(CSSPrimitiveValue.CSS_PX) - this.arrowToggleWidth;
else
start += this._listItemNode.totalOffsetLeft + computedStyle.getPropertyCSSValue("padding-left").getFloatValue(CSSPrimitiveValue.CSS_PX);
return event.pageX >= start && event.pageX <= start + this.arrowToggleWidth && this.hasChildren;
}
populateContextMenu(contextMenu, event)
{
if (this.children.some((child) => child.hasChildren) || (this.hasChildren && !this.children.length)) {
contextMenu.appendSeparator();
contextMenu.appendItem(WI.UIString("Expand All"), this.expandRecursively.bind(this));
contextMenu.appendItem(WI.UIString("Collapse All"), this.collapseRecursively.bind(this));
}
}
};
WI.TreeElement.Event = {
DoubleClick: "tree-element-double-click",
};