| /* |
| * 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", |
| }; |