mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 13:44:39 +08:00
Bug: 843951 Change-Id: I4e71d3745ce214f467e86a338aa03ef630a2f1b0 Reviewed-on: https://chromium-review.googlesource.com/1068201 Commit-Queue: Joel Einbinder <einbinder@chromium.org> Reviewed-by: Dmitry Gozman <dgozman@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#562666} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: e8591c53a16ed870806c419df59046be8da4d3ae
1257 lines
33 KiB
JavaScript
1257 lines
33 KiB
JavaScript
/*
|
|
* Copyright (C) 2007 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 Computer, 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.
|
|
*/
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
UI.TreeOutline = class extends Common.Object {
|
|
constructor() {
|
|
super();
|
|
this._createRootElement();
|
|
|
|
/** @type {?UI.TreeElement} */
|
|
this.selectedTreeElement = null;
|
|
this.expandTreeElementsWhenArrowing = false;
|
|
/** @type {?function(!UI.TreeElement, !UI.TreeElement):number} */
|
|
this._comparator = null;
|
|
|
|
this.contentElement = this._rootElement._childrenListNode;
|
|
this.contentElement.addEventListener('keydown', this._treeKeyDown.bind(this), true);
|
|
|
|
this._focusable = true;
|
|
this.setFocusable(this._focusable);
|
|
if (this._focusable)
|
|
this.contentElement.setAttribute('tabIndex', -1);
|
|
this.element = this.contentElement;
|
|
UI.ARIAUtils.markAsTree(this.element);
|
|
}
|
|
|
|
_createRootElement() {
|
|
this._rootElement = new UI.TreeElement();
|
|
this._rootElement.treeOutline = this;
|
|
this._rootElement.root = true;
|
|
this._rootElement.selectable = false;
|
|
this._rootElement.expanded = true;
|
|
this._rootElement._childrenListNode.classList.remove('children');
|
|
}
|
|
|
|
/**
|
|
* @return {!UI.TreeElement}
|
|
*/
|
|
rootElement() {
|
|
return this._rootElement;
|
|
}
|
|
|
|
/**
|
|
* @return {?UI.TreeElement}
|
|
*/
|
|
firstChild() {
|
|
return this._rootElement.firstChild();
|
|
}
|
|
|
|
/**
|
|
* @return {?UI.TreeElement}
|
|
*/
|
|
_lastDescendent() {
|
|
let last = this._rootElement.lastChild();
|
|
while (last.expanded && last.childCount())
|
|
last = last.lastChild();
|
|
return last;
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} child
|
|
*/
|
|
appendChild(child) {
|
|
this._rootElement.appendChild(child);
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} child
|
|
* @param {number} index
|
|
*/
|
|
insertChild(child, index) {
|
|
this._rootElement.insertChild(child, index);
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} child
|
|
*/
|
|
removeChild(child) {
|
|
this._rootElement.removeChild(child);
|
|
}
|
|
|
|
removeChildren() {
|
|
this._rootElement.removeChildren();
|
|
}
|
|
|
|
/**
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @return {?UI.TreeElement}
|
|
*/
|
|
treeElementFromPoint(x, y) {
|
|
const node = this.contentElement.ownerDocument.deepElementFromPoint(x, y);
|
|
if (!node)
|
|
return null;
|
|
|
|
const listNode = node.enclosingNodeOrSelfWithNodeNameInArray(['ol', 'li']);
|
|
if (listNode)
|
|
return listNode.parentTreeElement || listNode.treeElement;
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {?Event} event
|
|
* @return {?UI.TreeElement}
|
|
*/
|
|
treeElementFromEvent(event) {
|
|
return event ? this.treeElementFromPoint(event.pageX, event.pageY) : null;
|
|
}
|
|
|
|
/**
|
|
* @param {?function(!UI.TreeElement, !UI.TreeElement):number} comparator
|
|
*/
|
|
setComparator(comparator) {
|
|
this._comparator = comparator;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} focusable
|
|
*/
|
|
setFocusable(focusable) {
|
|
if (focusable) {
|
|
this._focusable = true;
|
|
this.contentElement.setAttribute('tabIndex', -1);
|
|
if (this.selectedTreeElement)
|
|
this.selectedTreeElement._setFocusable(true);
|
|
} else {
|
|
this._focusable = false;
|
|
this.contentElement.removeAttribute('tabIndex');
|
|
if (this.selectedTreeElement)
|
|
this.selectedTreeElement._setFocusable(false);
|
|
}
|
|
}
|
|
|
|
focus() {
|
|
if (this.selectedTreeElement)
|
|
this.selectedTreeElement.listItemElement.focus();
|
|
else
|
|
this.contentElement.focus();
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} element
|
|
*/
|
|
_bindTreeElement(element) {
|
|
if (element.treeOutline)
|
|
console.error('Binding element for the second time: ' + new Error().stack);
|
|
element.treeOutline = this;
|
|
element.onbind();
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} element
|
|
*/
|
|
_unbindTreeElement(element) {
|
|
if (!element.treeOutline)
|
|
console.error('Unbinding element that was not bound: ' + new Error().stack);
|
|
|
|
element.deselect();
|
|
element.onunbind();
|
|
element.treeOutline = null;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
selectPrevious() {
|
|
let nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true);
|
|
while (nextSelectedElement && !nextSelectedElement.selectable)
|
|
nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(!this.expandTreeElementsWhenArrowing);
|
|
if (!nextSelectedElement)
|
|
return false;
|
|
nextSelectedElement.select(false, true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
selectNext() {
|
|
let nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true);
|
|
while (nextSelectedElement && !nextSelectedElement.selectable)
|
|
nextSelectedElement = nextSelectedElement.traverseNextTreeElement(!this.expandTreeElementsWhenArrowing);
|
|
if (!nextSelectedElement)
|
|
return false;
|
|
nextSelectedElement.select(false, true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
_selectFirst() {
|
|
let first = this.firstChild();
|
|
while (first && !first.selectable)
|
|
first = first.traverseNextTreeElement(true);
|
|
if (!first)
|
|
return false;
|
|
first.select(false, true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
_selectLast() {
|
|
let last = this._lastDescendent();
|
|
while (last && !last.selectable)
|
|
last = last.traversePreviousTreeElement(true);
|
|
if (!last)
|
|
return false;
|
|
last.select(false, true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_treeKeyDown(event) {
|
|
if (!this.selectedTreeElement || event.target !== this.selectedTreeElement.listItemElement || event.shiftKey ||
|
|
event.metaKey || event.ctrlKey)
|
|
return;
|
|
|
|
let handled = false;
|
|
if (event.key === 'ArrowUp' && !event.altKey) {
|
|
handled = this.selectPrevious();
|
|
} else if (event.key === 'ArrowDown' && !event.altKey) {
|
|
handled = this.selectNext();
|
|
} else if (event.key === 'ArrowLeft') {
|
|
handled = this.selectedTreeElement.collapseOrAscend(event.altKey);
|
|
} else if (event.key === 'ArrowRight') {
|
|
if (!this.selectedTreeElement.revealed()) {
|
|
this.selectedTreeElement.reveal();
|
|
handled = true;
|
|
} else {
|
|
handled = this.selectedTreeElement.descendOrExpand(event.altKey);
|
|
}
|
|
} else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */) {
|
|
handled = this.selectedTreeElement.ondelete();
|
|
} else if (isEnterKey(event)) {
|
|
handled = this.selectedTreeElement.onenter();
|
|
} else if (event.keyCode === UI.KeyboardShortcut.Keys.Space.code) {
|
|
handled = this.selectedTreeElement.onspace();
|
|
} else if (event.key === 'Home') {
|
|
handled = this._selectFirst();
|
|
} else if (event.key === 'End') {
|
|
handled = this._selectLast();
|
|
}
|
|
|
|
if (handled)
|
|
event.consume(true);
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} treeElement
|
|
* @param {boolean} center
|
|
*/
|
|
_deferredScrollIntoView(treeElement, center) {
|
|
if (!this._treeElementToScrollIntoView)
|
|
this.element.window().requestAnimationFrame(deferredScrollIntoView.bind(this));
|
|
this._treeElementToScrollIntoView = treeElement;
|
|
this._centerUponScrollIntoView = center;
|
|
/**
|
|
* @this {UI.TreeOutline}
|
|
*/
|
|
function deferredScrollIntoView() {
|
|
this._treeElementToScrollIntoView.listItemElement.scrollIntoViewIfNeeded(this._centerUponScrollIntoView);
|
|
delete this._treeElementToScrollIntoView;
|
|
delete this._centerUponScrollIntoView;
|
|
}
|
|
}
|
|
};
|
|
|
|
/** @enum {symbol} */
|
|
UI.TreeOutline.Events = {
|
|
ElementAttached: Symbol('ElementAttached'),
|
|
ElementExpanded: Symbol('ElementExpanded'),
|
|
ElementCollapsed: Symbol('ElementCollapsed'),
|
|
ElementSelected: Symbol('ElementSelected')
|
|
};
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
UI.TreeOutlineInShadow = class extends UI.TreeOutline {
|
|
constructor() {
|
|
super();
|
|
this.contentElement.classList.add('tree-outline');
|
|
|
|
// Redefine element to the external one.
|
|
this.element = createElement('div');
|
|
this._shadowRoot = UI.createShadowRootWithCoreStyles(this.element, 'ui/treeoutline.css');
|
|
this._disclosureElement = this._shadowRoot.createChild('div', 'tree-outline-disclosure');
|
|
this._disclosureElement.appendChild(this.contentElement);
|
|
this._renderSelection = true;
|
|
}
|
|
|
|
/**
|
|
* @param {string} cssFile
|
|
*/
|
|
registerRequiredCSS(cssFile) {
|
|
UI.appendStyle(this._shadowRoot, cssFile);
|
|
}
|
|
|
|
hideOverflow() {
|
|
this._disclosureElement.classList.add('tree-outline-disclosure-hide-overflow');
|
|
}
|
|
|
|
makeDense() {
|
|
this.contentElement.classList.add('tree-outline-dense');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
UI.TreeElement = class {
|
|
/**
|
|
* @param {(string|!Node)=} title
|
|
* @param {boolean=} expandable
|
|
*/
|
|
constructor(title, expandable) {
|
|
/** @type {?UI.TreeOutline} */
|
|
this.treeOutline = null;
|
|
this.parent = null;
|
|
this.previousSibling = null;
|
|
this.nextSibling = null;
|
|
this._boundOnFocus = this._onFocus.bind(this);
|
|
this._boundOnBlur = this._onBlur.bind(this);
|
|
|
|
this._listItemNode = createElement('li');
|
|
this._titleElement = this._listItemNode.createChild('span', 'tree-element-title');
|
|
this._listItemNode.treeElement = this;
|
|
if (title)
|
|
this.title = title;
|
|
this._listItemNode.addEventListener('mousedown', this._handleMouseDown.bind(this), false);
|
|
this._listItemNode.addEventListener('click', this._treeElementToggled.bind(this), false);
|
|
this._listItemNode.addEventListener('dblclick', this._handleDoubleClick.bind(this), false);
|
|
UI.ARIAUtils.markAsTreeitem(this._listItemNode);
|
|
|
|
this._childrenListNode = createElement('ol');
|
|
this._childrenListNode.parentTreeElement = this;
|
|
this._childrenListNode.classList.add('children');
|
|
UI.ARIAUtils.markAsGroup(this._childrenListNode);
|
|
|
|
this._hidden = false;
|
|
this._selectable = true;
|
|
this.expanded = false;
|
|
this.selected = false;
|
|
this.setExpandable(expandable || false);
|
|
this._collapsible = true;
|
|
}
|
|
|
|
/**
|
|
* @param {?UI.TreeElement} ancestor
|
|
* @return {boolean}
|
|
*/
|
|
hasAncestor(ancestor) {
|
|
if (!ancestor)
|
|
return false;
|
|
|
|
let currentNode = this.parent;
|
|
while (currentNode) {
|
|
if (ancestor === currentNode)
|
|
return true;
|
|
currentNode = currentNode.parent;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {?UI.TreeElement} ancestor
|
|
* @return {boolean}
|
|
*/
|
|
hasAncestorOrSelf(ancestor) {
|
|
return this === ancestor || this.hasAncestor(ancestor);
|
|
}
|
|
|
|
/**
|
|
* @return {!Array.<!UI.TreeElement>}
|
|
*/
|
|
children() {
|
|
return this._children || [];
|
|
}
|
|
|
|
/**
|
|
* @return {number}
|
|
*/
|
|
childCount() {
|
|
return this._children ? this._children.length : 0;
|
|
}
|
|
|
|
/**
|
|
* @return {?UI.TreeElement}
|
|
*/
|
|
firstChild() {
|
|
return this._children ? this._children[0] : null;
|
|
}
|
|
|
|
/**
|
|
* @return {?UI.TreeElement}
|
|
*/
|
|
lastChild() {
|
|
return this._children ? this._children[this._children.length - 1] : null;
|
|
}
|
|
|
|
/**
|
|
* @param {number} index
|
|
* @return {?UI.TreeElement}
|
|
*/
|
|
childAt(index) {
|
|
return this._children ? this._children[index] : null;
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} child
|
|
* @return {number}
|
|
*/
|
|
indexOfChild(child) {
|
|
return this._children ? this._children.indexOf(child) : -1;
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} child
|
|
*/
|
|
appendChild(child) {
|
|
if (!this._children)
|
|
this._children = [];
|
|
|
|
let insertionIndex;
|
|
if (this.treeOutline && this.treeOutline._comparator)
|
|
insertionIndex = this._children.lowerBound(child, this.treeOutline._comparator);
|
|
else
|
|
insertionIndex = this._children.length;
|
|
this.insertChild(child, insertionIndex);
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} child
|
|
* @param {number} index
|
|
*/
|
|
insertChild(child, index) {
|
|
if (!this._children)
|
|
this._children = [];
|
|
|
|
if (!child)
|
|
throw 'child can\'t be undefined or null';
|
|
|
|
console.assert(
|
|
!child.parent, 'Attempting to insert a child that is already in the tree, reparenting is not supported.');
|
|
|
|
const previousChild = (index > 0 ? this._children[index - 1] : null);
|
|
if (previousChild) {
|
|
previousChild.nextSibling = child;
|
|
child.previousSibling = previousChild;
|
|
} else {
|
|
child.previousSibling = null;
|
|
}
|
|
|
|
const nextChild = this._children[index];
|
|
if (nextChild) {
|
|
nextChild.previousSibling = child;
|
|
child.nextSibling = nextChild;
|
|
} else {
|
|
child.nextSibling = null;
|
|
}
|
|
|
|
this._children.splice(index, 0, child);
|
|
|
|
this.setExpandable(true);
|
|
child.parent = this;
|
|
|
|
if (this.treeOutline)
|
|
this.treeOutline._bindTreeElement(child);
|
|
for (let current = child.firstChild(); this.treeOutline && current;
|
|
current = current.traverseNextTreeElement(false, child, true))
|
|
this.treeOutline._bindTreeElement(current);
|
|
child.onattach();
|
|
child._ensureSelection();
|
|
if (this.treeOutline)
|
|
this.treeOutline.dispatchEventToListeners(UI.TreeOutline.Events.ElementAttached, child);
|
|
const nextSibling = child.nextSibling ? child.nextSibling._listItemNode : null;
|
|
this._childrenListNode.insertBefore(child._listItemNode, nextSibling);
|
|
this._childrenListNode.insertBefore(child._childrenListNode, nextSibling);
|
|
if (child.selected)
|
|
child.select();
|
|
if (child.expanded)
|
|
child.expand();
|
|
}
|
|
|
|
/**
|
|
* @param {number} childIndex
|
|
*/
|
|
removeChildAtIndex(childIndex) {
|
|
if (childIndex < 0 || childIndex >= this._children.length)
|
|
throw 'childIndex out of range';
|
|
|
|
const child = this._children[childIndex];
|
|
this._children.splice(childIndex, 1);
|
|
|
|
const parent = child.parent;
|
|
if (this.treeOutline && this.treeOutline.selectedTreeElement &&
|
|
this.treeOutline.selectedTreeElement.hasAncestorOrSelf(child)) {
|
|
if (child.nextSibling)
|
|
child.nextSibling.select(true);
|
|
else if (child.previousSibling)
|
|
child.previousSibling.select(true);
|
|
else if (parent)
|
|
parent.select(true);
|
|
}
|
|
|
|
if (child.previousSibling)
|
|
child.previousSibling.nextSibling = child.nextSibling;
|
|
if (child.nextSibling)
|
|
child.nextSibling.previousSibling = child.previousSibling;
|
|
child.parent = null;
|
|
|
|
if (this.treeOutline)
|
|
this.treeOutline._unbindTreeElement(child);
|
|
for (let current = child.firstChild(); this.treeOutline && current;
|
|
current = current.traverseNextTreeElement(false, child, true))
|
|
this.treeOutline._unbindTreeElement(current);
|
|
|
|
child._detach();
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.TreeElement} child
|
|
*/
|
|
removeChild(child) {
|
|
if (!child)
|
|
throw 'child can\'t be undefined or null';
|
|
if (child.parent !== this)
|
|
return;
|
|
|
|
const childIndex = this._children.indexOf(child);
|
|
if (childIndex === -1)
|
|
throw 'child not found in this node\'s children';
|
|
|
|
this.removeChildAtIndex(childIndex);
|
|
}
|
|
|
|
removeChildren() {
|
|
if (!this.root && this.treeOutline && this.treeOutline.selectedTreeElement &&
|
|
this.treeOutline.selectedTreeElement.hasAncestorOrSelf(this))
|
|
this.select(true);
|
|
|
|
for (let i = 0; this._children && i < this._children.length; ++i) {
|
|
const child = this._children[i];
|
|
child.previousSibling = null;
|
|
child.nextSibling = null;
|
|
child.parent = null;
|
|
|
|
if (this.treeOutline)
|
|
this.treeOutline._unbindTreeElement(child);
|
|
for (let current = child.firstChild(); this.treeOutline && current;
|
|
current = current.traverseNextTreeElement(false, child, true))
|
|
this.treeOutline._unbindTreeElement(current);
|
|
child._detach();
|
|
}
|
|
this._children = [];
|
|
}
|
|
|
|
get selectable() {
|
|
if (this._hidden)
|
|
return false;
|
|
return this._selectable;
|
|
}
|
|
|
|
set selectable(x) {
|
|
this._selectable = x;
|
|
}
|
|
|
|
get listItemElement() {
|
|
return this._listItemNode;
|
|
}
|
|
|
|
/**
|
|
* @return {!Element}
|
|
*/
|
|
titleElement() {
|
|
return this._titleElement;
|
|
}
|
|
|
|
get childrenListElement() {
|
|
return this._childrenListNode;
|
|
}
|
|
|
|
/**
|
|
* @return {string|!Node}
|
|
*/
|
|
get title() {
|
|
return this._title;
|
|
}
|
|
|
|
/**
|
|
* @param {string|!Node} x
|
|
*/
|
|
set title(x) {
|
|
if (this._title === x)
|
|
return;
|
|
this._title = x;
|
|
|
|
if (typeof x === 'string') {
|
|
this._titleElement.textContent = x;
|
|
this.tooltip = x;
|
|
} else {
|
|
this._titleElement = x;
|
|
this.tooltip = '';
|
|
}
|
|
|
|
this._listItemNode.removeChildren();
|
|
if (this._leadingIconsElement)
|
|
this._listItemNode.appendChild(this._leadingIconsElement);
|
|
this._listItemNode.appendChild(this._titleElement);
|
|
if (this._trailingIconsElement)
|
|
this._listItemNode.appendChild(this._trailingIconsElement);
|
|
this._ensureSelection();
|
|
}
|
|
|
|
/**
|
|
* @return {string}
|
|
*/
|
|
titleAsText() {
|
|
if (!this._title)
|
|
return '';
|
|
if (typeof this._title === 'string')
|
|
return this._title;
|
|
return this._title.textContent;
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.InplaceEditor.Config} editingConfig
|
|
*/
|
|
startEditingTitle(editingConfig) {
|
|
UI.InplaceEditor.startEditing(this._titleElement, editingConfig);
|
|
this.treeOutline._shadowRoot.getSelection().selectAllChildren(this._titleElement);
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<!UI.Icon>} icons
|
|
*/
|
|
setLeadingIcons(icons) {
|
|
if (!this._leadingIconsElement && !icons.length)
|
|
return;
|
|
if (!this._leadingIconsElement) {
|
|
this._leadingIconsElement = createElementWithClass('div', 'leading-icons');
|
|
this._leadingIconsElement.classList.add('icons-container');
|
|
this._listItemNode.insertBefore(this._leadingIconsElement, this._titleElement);
|
|
this._ensureSelection();
|
|
}
|
|
this._leadingIconsElement.removeChildren();
|
|
for (const icon of icons)
|
|
this._leadingIconsElement.appendChild(icon);
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<!UI.Icon>} icons
|
|
*/
|
|
setTrailingIcons(icons) {
|
|
if (!this._trailingIconsElement && !icons.length)
|
|
return;
|
|
if (!this._trailingIconsElement) {
|
|
this._trailingIconsElement = createElementWithClass('div', 'trailing-icons');
|
|
this._trailingIconsElement.classList.add('icons-container');
|
|
this._listItemNode.appendChild(this._trailingIconsElement);
|
|
this._ensureSelection();
|
|
}
|
|
this._trailingIconsElement.removeChildren();
|
|
for (const icon of icons)
|
|
this._trailingIconsElement.appendChild(icon);
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {string}
|
|
*/
|
|
get tooltip() {
|
|
return this._tooltip || '';
|
|
}
|
|
|
|
/**
|
|
* @param {string} x
|
|
*/
|
|
set tooltip(x) {
|
|
if (this._tooltip === x)
|
|
return;
|
|
this._tooltip = x;
|
|
this._listItemNode.title = x;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
isExpandable() {
|
|
return this._expandable;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} expandable
|
|
*/
|
|
setExpandable(expandable) {
|
|
if (this._expandable === expandable)
|
|
return;
|
|
|
|
this._expandable = expandable;
|
|
|
|
this._listItemNode.classList.toggle('parent', expandable);
|
|
if (!expandable) {
|
|
this.collapse();
|
|
UI.ARIAUtils.unsetExpanded(this._listItemNode);
|
|
} else {
|
|
UI.ARIAUtils.setExpanded(this._listItemNode, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} collapsible
|
|
*/
|
|
setCollapsible(collapsible) {
|
|
if (this._collapsible === collapsible)
|
|
return;
|
|
|
|
this._collapsible = collapsible;
|
|
|
|
this._listItemNode.classList.toggle('always-parent', !collapsible);
|
|
if (!collapsible)
|
|
this.expand();
|
|
}
|
|
|
|
get hidden() {
|
|
return this._hidden;
|
|
}
|
|
|
|
set hidden(x) {
|
|
if (this._hidden === x)
|
|
return;
|
|
|
|
this._hidden = x;
|
|
|
|
this._listItemNode.classList.toggle('hidden', x);
|
|
this._childrenListNode.classList.toggle('hidden', x);
|
|
}
|
|
|
|
invalidateChildren() {
|
|
if (this._children) {
|
|
this.removeChildren();
|
|
this._children = null;
|
|
}
|
|
}
|
|
|
|
|
|
_ensureSelection() {
|
|
if (!this.treeOutline || !this.treeOutline._renderSelection)
|
|
return;
|
|
if (!this._selectionElement)
|
|
this._selectionElement = createElementWithClass('div', 'selection fill');
|
|
this._listItemNode.insertBefore(this._selectionElement, this.listItemElement.firstChild);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_treeElementToggled(event) {
|
|
const element = event.currentTarget;
|
|
if (element.treeElement !== this || element.hasSelection())
|
|
return;
|
|
|
|
const toggleOnClick = this.toggleOnClick && !this.selectable;
|
|
const isInTriangle = this.isEventWithinDisclosureTriangle(event);
|
|
if (!toggleOnClick && !isInTriangle)
|
|
return;
|
|
|
|
if (this.expanded) {
|
|
if (event.altKey)
|
|
this.collapseRecursively();
|
|
else
|
|
this.collapse();
|
|
} else {
|
|
if (event.altKey)
|
|
this.expandRecursively();
|
|
else
|
|
this.expand();
|
|
}
|
|
event.consume();
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_handleMouseDown(event) {
|
|
const element = event.currentTarget;
|
|
if (!element)
|
|
return;
|
|
if (!this.selectable)
|
|
return;
|
|
if (element.treeElement !== this)
|
|
return;
|
|
|
|
if (this.isEventWithinDisclosureTriangle(event))
|
|
return;
|
|
|
|
this.selectOnMouseDown(event);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_handleDoubleClick(event) {
|
|
const element = event.currentTarget;
|
|
if (!element || element.treeElement !== this)
|
|
return;
|
|
|
|
const handled = this.ondblclick(event);
|
|
if (handled)
|
|
return;
|
|
if (this._expandable && !this.expanded)
|
|
this.expand();
|
|
}
|
|
|
|
_detach() {
|
|
this._listItemNode.remove();
|
|
this._childrenListNode.remove();
|
|
}
|
|
|
|
collapse() {
|
|
if (!this.expanded || !this._collapsible)
|
|
return;
|
|
this._listItemNode.classList.remove('expanded');
|
|
this._childrenListNode.classList.remove('expanded');
|
|
UI.ARIAUtils.setExpanded(this._listItemNode, false);
|
|
this.expanded = false;
|
|
this.oncollapse();
|
|
if (this.treeOutline)
|
|
this.treeOutline.dispatchEventToListeners(UI.TreeOutline.Events.ElementCollapsed, this);
|
|
}
|
|
|
|
collapseRecursively() {
|
|
let item = this;
|
|
while (item) {
|
|
if (item.expanded)
|
|
item.collapse();
|
|
item = item.traverseNextTreeElement(false, this, true);
|
|
}
|
|
}
|
|
|
|
collapseChildren() {
|
|
if (!this._children)
|
|
return;
|
|
for (const child of this._children)
|
|
child.collapseRecursively();
|
|
}
|
|
|
|
expand() {
|
|
if (!this._expandable || (this.expanded && this._children))
|
|
return;
|
|
|
|
// Set this before onpopulate. Since onpopulate can add elements, this makes
|
|
// sure the expanded flag is true before calling those functions. This prevents the possibility
|
|
// of an infinite loop if onpopulate were to call expand.
|
|
|
|
this.expanded = true;
|
|
|
|
this._populateIfNeeded();
|
|
this._listItemNode.classList.add('expanded');
|
|
this._childrenListNode.classList.add('expanded');
|
|
UI.ARIAUtils.setExpanded(this._listItemNode, true);
|
|
|
|
if (this.treeOutline) {
|
|
this.onexpand();
|
|
this.treeOutline.dispatchEventToListeners(UI.TreeOutline.Events.ElementExpanded, this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number=} maxDepth
|
|
*/
|
|
expandRecursively(maxDepth) {
|
|
let item = this;
|
|
const info = {};
|
|
let 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 (isNaN(maxDepth))
|
|
maxDepth = 3;
|
|
|
|
while (item) {
|
|
if (depth < maxDepth)
|
|
item.expand();
|
|
item = item.traverseNextTreeElement(false, this, (depth >= maxDepth), info);
|
|
depth += info.depthChange;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} altKey
|
|
* @return {boolean}
|
|
*/
|
|
collapseOrAscend(altKey) {
|
|
if (this.expanded) {
|
|
if (altKey)
|
|
this.collapseRecursively();
|
|
else
|
|
this.collapse();
|
|
return true;
|
|
}
|
|
|
|
if (!this.parent || this.parent.root)
|
|
return false;
|
|
|
|
if (!this.parent.selectable) {
|
|
this.parent.collapse();
|
|
return true;
|
|
}
|
|
|
|
let nextSelectedElement = this.parent;
|
|
while (nextSelectedElement && !nextSelectedElement.selectable)
|
|
nextSelectedElement = nextSelectedElement.parent;
|
|
|
|
if (!nextSelectedElement)
|
|
return false;
|
|
nextSelectedElement.select(false, true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} altKey
|
|
* @return {boolean}
|
|
*/
|
|
descendOrExpand(altKey) {
|
|
if (!this._expandable)
|
|
return false;
|
|
|
|
if (!this.expanded) {
|
|
if (altKey)
|
|
this.expandRecursively();
|
|
else
|
|
this.expand();
|
|
return true;
|
|
}
|
|
|
|
let nextSelectedElement = this.firstChild();
|
|
while (nextSelectedElement && !nextSelectedElement.selectable)
|
|
nextSelectedElement = nextSelectedElement.nextSibling;
|
|
|
|
if (!nextSelectedElement)
|
|
return false;
|
|
nextSelectedElement.select(false, true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean=} center
|
|
*/
|
|
reveal(center) {
|
|
let currentAncestor = this.parent;
|
|
while (currentAncestor && !currentAncestor.root) {
|
|
if (!currentAncestor.expanded)
|
|
currentAncestor.expand();
|
|
currentAncestor = currentAncestor.parent;
|
|
}
|
|
|
|
this.treeOutline._deferredScrollIntoView(this, !!center);
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
revealed() {
|
|
let currentAncestor = this.parent;
|
|
while (currentAncestor && !currentAncestor.root) {
|
|
if (!currentAncestor.expanded)
|
|
return false;
|
|
currentAncestor = currentAncestor.parent;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
selectOnMouseDown(event) {
|
|
if (this.select(false, true))
|
|
event.consume(true);
|
|
|
|
if (this._listItemNode.draggable && this._selectionElement) {
|
|
const marginLeft =
|
|
this.treeOutline.element.getBoundingClientRect().left - this._listItemNode.getBoundingClientRect().left;
|
|
// By default the left margin extends far off screen. This is not a problem except when dragging an element.
|
|
// Setting the margin once here should be fine, because we believe the left margin should never change.
|
|
this._selectionElement.style.setProperty('margin-left', marginLeft + 'px');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean=} omitFocus
|
|
* @param {boolean=} selectedByUser
|
|
* @return {boolean}
|
|
*/
|
|
select(omitFocus, selectedByUser) {
|
|
if (!this.treeOutline || !this.selectable || this.selected)
|
|
return false;
|
|
// Wait to deselect this element so that focus only changes once
|
|
const lastSelected = this.treeOutline.selectedTreeElement;
|
|
this.treeOutline.selectedTreeElement = null;
|
|
|
|
if (this.treeOutline._rootElement === this) {
|
|
if (lastSelected)
|
|
lastSelected.deselect();
|
|
return false;
|
|
}
|
|
|
|
this.selected = true;
|
|
|
|
this.treeOutline.selectedTreeElement = this;
|
|
if (this.treeOutline._focusable)
|
|
this._setFocusable(true);
|
|
if (!omitFocus || this.treeOutline.contentElement.hasFocus())
|
|
this.listItemElement.focus();
|
|
|
|
this._listItemNode.classList.add('selected');
|
|
this.treeOutline.dispatchEventToListeners(UI.TreeOutline.Events.ElementSelected, this);
|
|
if (lastSelected)
|
|
lastSelected.deselect();
|
|
return this.onselect(selectedByUser);
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} focusable
|
|
*/
|
|
_setFocusable(focusable) {
|
|
if (focusable) {
|
|
this._listItemNode.setAttribute('tabIndex', 0);
|
|
this._listItemNode.addEventListener('focus', this._boundOnFocus, false);
|
|
this._listItemNode.addEventListener('blur', this._boundOnBlur, false);
|
|
} else {
|
|
this._listItemNode.removeAttribute('tabIndex');
|
|
this._listItemNode.removeEventListener('focus', this._boundOnFocus, false);
|
|
this._listItemNode.removeEventListener('blur', this._boundOnBlur, false);
|
|
}
|
|
}
|
|
|
|
_onFocus() {
|
|
this._listItemNode.classList.add('force-white-icons');
|
|
}
|
|
|
|
_onBlur() {
|
|
this._listItemNode.classList.remove('force-white-icons');
|
|
}
|
|
|
|
/**
|
|
* @param {boolean=} omitFocus
|
|
*/
|
|
revealAndSelect(omitFocus) {
|
|
this.reveal(true);
|
|
this.select(omitFocus);
|
|
}
|
|
|
|
deselect() {
|
|
const hadFocus = this._listItemNode.hasFocus();
|
|
this.selected = false;
|
|
this._listItemNode.classList.remove('selected');
|
|
this._setFocusable(false);
|
|
|
|
if (this.treeOutline && this.treeOutline.selectedTreeElement === this) {
|
|
this.treeOutline.selectedTreeElement = null;
|
|
if (hadFocus)
|
|
this.treeOutline.focus();
|
|
}
|
|
}
|
|
|
|
_populateIfNeeded() {
|
|
if (this.treeOutline && this._expandable && !this._children) {
|
|
this._children = [];
|
|
this.onpopulate();
|
|
}
|
|
}
|
|
|
|
onpopulate() {
|
|
// Overridden by subclasses.
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
onenter() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
ondelete() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
onspace() {
|
|
return false;
|
|
}
|
|
|
|
onbind() {
|
|
}
|
|
|
|
onunbind() {
|
|
}
|
|
|
|
onattach() {
|
|
}
|
|
|
|
onexpand() {
|
|
}
|
|
|
|
oncollapse() {
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} e
|
|
* @return {boolean}
|
|
*/
|
|
ondblclick(e) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean=} selectedByUser
|
|
* @return {boolean}
|
|
*/
|
|
onselect(selectedByUser) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} skipUnrevealed
|
|
* @param {?UI.TreeElement=} stayWithin
|
|
* @param {boolean=} dontPopulate
|
|
* @param {!Object=} info
|
|
* @return {?UI.TreeElement}
|
|
*/
|
|
traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate, info) {
|
|
if (!dontPopulate)
|
|
this._populateIfNeeded();
|
|
|
|
if (info)
|
|
info.depthChange = 0;
|
|
|
|
let element = skipUnrevealed ? (this.revealed() ? this.firstChild() : null) : this.firstChild();
|
|
if (element && (!skipUnrevealed || (skipUnrevealed && this.expanded))) {
|
|
if (info)
|
|
info.depthChange = 1;
|
|
return element;
|
|
}
|
|
|
|
if (this === stayWithin)
|
|
return null;
|
|
|
|
element = skipUnrevealed ? (this.revealed() ? this.nextSibling : null) : this.nextSibling;
|
|
if (element)
|
|
return element;
|
|
|
|
element = this;
|
|
while (element && !element.root &&
|
|
!(skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) &&
|
|
element.parent !== stayWithin) {
|
|
if (info)
|
|
info.depthChange -= 1;
|
|
element = element.parent;
|
|
}
|
|
|
|
if (!element || element.root)
|
|
return null;
|
|
|
|
return (skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling);
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} skipUnrevealed
|
|
* @param {boolean=} dontPopulate
|
|
* @return {?UI.TreeElement}
|
|
*/
|
|
traversePreviousTreeElement(skipUnrevealed, dontPopulate) {
|
|
let element = skipUnrevealed ? (this.revealed() ? this.previousSibling : null) : this.previousSibling;
|
|
if (!dontPopulate && element)
|
|
element._populateIfNeeded();
|
|
|
|
while (element && (skipUnrevealed ? (element.revealed() && element.expanded ? element.lastChild() : null) :
|
|
element.lastChild())) {
|
|
if (!dontPopulate)
|
|
element._populateIfNeeded();
|
|
element =
|
|
(skipUnrevealed ? (element.revealed() && element.expanded ? element.lastChild() : null) :
|
|
element.lastChild());
|
|
}
|
|
|
|
if (element)
|
|
return element;
|
|
|
|
if (!this.parent || this.parent.root)
|
|
return null;
|
|
|
|
return this.parent;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
isEventWithinDisclosureTriangle(event) {
|
|
// FIXME: We should not use getComputedStyle(). For that we need to get rid of using ::before for disclosure triangle. (http://webk.it/74446)
|
|
const paddingLeftValue = window.getComputedStyle(this._listItemNode).paddingLeft;
|
|
console.assert(paddingLeftValue.endsWith('px'));
|
|
const computedLeftPadding = parseFloat(paddingLeftValue);
|
|
const left = this._listItemNode.totalOffsetLeft() + computedLeftPadding;
|
|
return event.pageX >= left && event.pageX <= left + UI.TreeElement._ArrowToggleWidth && this._expandable;
|
|
}
|
|
};
|
|
|
|
/** @const */
|
|
UI.TreeElement._ArrowToggleWidth = 10;
|
|
|
|
(function() {
|
|
const img = new Image();
|
|
if (window.devicePixelRatio > 1)
|
|
img.src = 'Images/treeoutlineTriangles_2x.png';
|
|
else
|
|
img.src = 'Images/treeoutlineTriangles.png';
|
|
UI.TreeElement._imagePreload = img;
|
|
})();
|