/* * Copyright (C) 2007-2020 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.TreeOutline = class TreeOutline extends WI.Object { constructor(selectable = true) { super(); this.element = document.createElement("ol"); this.element.classList.add(WI.TreeOutline.ElementStyleClassName); this.element.role = "tree"; this.element.addEventListener("contextmenu", this._handleContextmenu.bind(this)); this.children = []; this._childrenListNode = this.element; this._childrenListNode.removeChildren(); this._knownTreeElements = []; this._treeElementsExpandedState = []; this.allowsRepeatSelection = false; this.root = true; this.hasChildren = false; this.expanded = true; this.selected = false; this.treeOutline = this; this._hidden = false; this._compact = false; this._large = false; this._disclosureButtons = true; this._customIndent = false; this._selectable = selectable; this._cachedNumberOfDescendants = 0; let itemForRepresentedObject = this.getCachedTreeElement.bind(this); let selectionComparator = WI.SelectionController.createTreeComparator(itemForRepresentedObject); this._selectionController = new WI.SelectionController(this, selectionComparator); this._itemWasSelectedByUser = false; this._processingSelectionChange = false; this._suppressNextSelectionDidChangeEvent = false; this._virtualizedDebouncer = null; this._virtualizedVisibleTreeElements = null; this._virtualizedAttachedTreeElements = null; this._virtualizedScrollContainer = null; this._virtualizedTreeItemHeight = NaN; this._virtualizedTopSpacer = null; this._virtualizedBottomSpacer = null; this._childrenListNode.tabIndex = 0; this._childrenListNode.addEventListener("keydown", this._treeKeyDown.bind(this), true); this._childrenListNode.addEventListener("mousedown", this._handleMouseDown.bind(this)); WI.TreeOutline._generateStyleRulesIfNeeded(); if (!this._selectable) this.element.classList.add("non-selectable"); } // Public get allowsEmptySelection() { return this._selectionController.allowsEmptySelection; } set allowsEmptySelection(flag) { this._selectionController.allowsEmptySelection = flag; } get allowsMultipleSelection() { return this._selectionController.allowsMultipleSelection; } set allowsMultipleSelection(flag) { this._selectionController.allowsMultipleSelection = flag; } get selectedTreeElement() { return this.getCachedTreeElement(this._selectionController.lastSelectedItem); } set selectedTreeElement(treeElement) { if (treeElement) this._selectionController.selectItem(this.objectForSelection(treeElement)); else this._selectionController.deselectAll(); } get selectedTreeElements() { if (this.allowsMultipleSelection) { let treeElements = []; for (let representedObject of this._selectionController.selectedItems) treeElements.push(this.getCachedTreeElement(representedObject)); return treeElements; } let selectedTreeElement = this.selectedTreeElement; if (selectedTreeElement) return [selectedTreeElement]; return []; } get processingSelectionChange() { return this._processingSelectionChange; } get hidden() { return this._hidden; } set hidden(x) { if (this._hidden === x) return; this._hidden = x; this.element.hidden = this._hidden; } get compact() { return this._compact; } set compact(x) { if (this._compact === x) return; this._compact = x; if (this._compact) this.large = false; this.element.classList.toggle("compact", this._compact); } get large() { return this._large; } set large(x) { if (this._large === x) return; this._large = x; if (this._large) this.compact = false; this.element.classList.toggle("large", this._large); } get disclosureButtons() { return this._disclosureButtons; } set disclosureButtons(x) { if (this._disclosureButtons === x) return; this._disclosureButtons = x; this.element.classList.toggle("hide-disclosure-buttons", !this._disclosureButtons); } get customIndent() { return this._customIndent; } set customIndent(x) { if (this._customIndent === x) return; this._customIndent = x; this.element.classList.toggle(WI.TreeOutline.CustomIndentStyleClassName, this._customIndent); } get selectable() { return this._selectable; } appendChild(child) { console.assert(child); if (!child) return; var lastChild = this.children[this.children.length - 1]; if (lastChild) { lastChild.nextSibling = child; child.previousSibling = lastChild; } else { child.previousSibling = null; child.nextSibling = null; } var isFirstChild = !this.children.length; this.children.push(child); this.hasChildren = true; child.parent = this; child.treeOutline = this.treeOutline; child.treeOutline._rememberTreeElement(child); var current = child.children[0]; while (current) { current.treeOutline = this.treeOutline; current.treeOutline._rememberTreeElement(current); current = current.traverseNextTreeElement(false, child, true); } if (child.hasChildren && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined) child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier]; if (this._childrenListNode) child._attach(); if (this.treeOutline) this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementAdded, {element: child}); if (isFirstChild && this.expanded) this.expand(); } insertChild(child, index) { console.assert(child); if (!child) return; var previousChild = index > 0 ? this.children[index - 1] : null; if (previousChild) { previousChild.nextSibling = child; child.previousSibling = previousChild; } else { child.previousSibling = null; } var nextChild = this.children[index]; if (nextChild) { nextChild.previousSibling = child; child.nextSibling = nextChild; } else { child.nextSibling = null; } var isFirstChild = !this.children.length; this.children.splice(index, 0, child); this.hasChildren = true; child.parent = this; child.treeOutline = this.treeOutline; child.treeOutline._rememberTreeElement(child); var current = child.children[0]; while (current) { current.treeOutline = this.treeOutline; current.treeOutline._rememberTreeElement(current); current = current.traverseNextTreeElement(false, child, true); } if (child.expandable && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined) child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier]; if (this._childrenListNode) child._attach(); if (this.treeOutline) this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementAdded, {element: child}); if (isFirstChild && this.expanded) this.expand(); } removeChildAtIndex(childIndex, suppressOnDeselect, suppressSelectSibling) { console.assert(childIndex >= 0 && childIndex < this.children.length); if (childIndex < 0 || childIndex >= this.children.length) return; let child = this.children[childIndex]; let parent = child.parent; let childOrDescendantWasSelected = child.deselect(suppressOnDeselect) || child.selfOrDescendant((descendant) => descendant.selected); if (childOrDescendantWasSelected && !suppressSelectSibling) { const omitFocus = true; if (child.previousSibling) child.previousSibling.select(omitFocus); else if (child.nextSibling) child.nextSibling.select(omitFocus); else parent.select(omitFocus); } let treeOutline = child.treeOutline; if (treeOutline) { treeOutline._forgetTreeElement(child); treeOutline._forgetChildrenRecursive(child); } if (child.previousSibling) child.previousSibling.nextSibling = child.nextSibling; if (child.nextSibling) child.nextSibling.previousSibling = child.previousSibling; this.children.splice(childIndex, 1); child._detach(); child.treeOutline = null; child.parent = null; child.nextSibling = null; child.previousSibling = null; if (treeOutline) treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementRemoved, {element: child}); } removeChild(child, suppressOnDeselect, suppressSelectSibling) { console.assert(child); if (!child) return; var childIndex = this.children.indexOf(child); console.assert(childIndex !== -1); if (childIndex === -1) return; this.removeChildAtIndex(childIndex, suppressOnDeselect, suppressSelectSibling); if (!this.children.length) { if (this._listItemNode) this._listItemNode.classList.remove("parent"); this.hasChildren = false; } } removeChildren(suppressOnDeselect) { for (let child of this.children) { child.deselect(suppressOnDeselect); let treeOutline = child.treeOutline; if (treeOutline) { treeOutline._forgetTreeElement(child); treeOutline._forgetChildrenRecursive(child); } child._detach(); child.treeOutline = null; child.parent = null; child.nextSibling = null; child.previousSibling = null; if (treeOutline) treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementRemoved, {element: child}); } this.children = []; } _rememberTreeElement(element) { if (!this._knownTreeElements[element.identifier]) this._knownTreeElements[element.identifier] = []; var elements = this._knownTreeElements[element.identifier]; if (!elements.includes(element)) { elements.push(element); this._cachedNumberOfDescendants++; } if (this.virtualized) this._virtualizedDebouncer.delayForFrame(); } _forgetTreeElement(element) { if (this.selectedTreeElement === element) { element.deselect(true); this.selectedTreeElement = null; } if (this._knownTreeElements[element.identifier]) { if (this._knownTreeElements[element.identifier].remove(element)) this._cachedNumberOfDescendants--; } if (this.virtualized) this._virtualizedDebouncer.delayForFrame(); } _forgetChildrenRecursive(parentElement) { var child = parentElement.children[0]; while (child) { this._forgetTreeElement(child); child = child.traverseNextTreeElement(false, parentElement, true); } } getCachedTreeElement(representedObject) { if (!representedObject) return null; // SelectionController requires every selectable object to be unique. // A TreeOutline subclass where multiple TreeElements may be associated // with one represented object can override objectForSelection, and return // a proxy object that is associated with a single TreeElement. if (representedObject.__proxyObjectTreeElement) return representedObject.__proxyObjectTreeElement; if (representedObject.__treeElementIdentifier) { // If this representedObject has a tree element identifier, and it is a known TreeElement // in our tree we can just return that tree element. var elements = this._knownTreeElements[representedObject.__treeElementIdentifier]; if (elements) { for (var i = 0; i < elements.length; ++i) if (elements[i].representedObject === representedObject) return elements[i]; } } return null; } selfOrDescendant(predicate) { let treeElements = [this]; while (treeElements.length) { let treeElement = treeElements.shift(); if (predicate(treeElement)) return treeElement; treeElements.pushAll(treeElement.children); } return false; } findTreeElement(representedObject, isAncestor, getParent) { if (!representedObject) return null; var cachedElement = this.getCachedTreeElement(representedObject); if (cachedElement) return cachedElement; // The representedObject isn't known, so we start at the top of the tree and work down to find the first // tree element that represents representedObject or one of its ancestors. var item; var found = false; for (var i = 0; i < this.children.length; ++i) { item = this.children[i]; if (item.representedObject === representedObject || (isAncestor && isAncestor(item.representedObject, representedObject))) { found = true; break; } } if (!found) return null; // Make sure the item that we found is connected to the root of the tree. // Build up a list of representedObject's ancestors that aren't already in our tree. var ancestors = []; var currentObject = representedObject; while (currentObject) { ancestors.unshift(currentObject); if (currentObject === item.representedObject) break; currentObject = getParent(currentObject); } // For each of those ancestors we populate them to fill in the tree. for (var i = 0; i < ancestors.length; ++i) { // Make sure we don't call findTreeElement with the same representedObject // again, to prevent infinite recursion. if (ancestors[i] === representedObject) continue; // FIXME: we could do something faster than findTreeElement since we will know the next // ancestor exists in the tree. item = this.findTreeElement(ancestors[i], isAncestor, getParent); if (!item) return null; item.onpopulate(); } return this.getCachedTreeElement(representedObject); } _treeElementDidChange(treeElement) { if (treeElement.treeOutline !== this) return; this.dispatchEventToListeners(WI.TreeOutline.Event.ElementDidChange, {element: treeElement}); } treeElementFromNode(node) { var listNode = node.closest("ol, li"); if (listNode) return listNode.parentTreeElement || listNode.treeElement; return null; } treeElementFromPoint(x, y) { var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y); if (!node) return null; return this.treeElementFromNode(node); } _treeKeyDown(event) { if (WI.isBeingEdited(event.target)) return; if (event.target !== this._childrenListNode && event.target.closest("." + WI.TreeOutline.ElementStyleClassName) !== this._childrenListNode) return; let isRTL = WI.resolveLayoutDirectionForElement(this.element) === WI.LayoutDirection.RTL; let expandKeyIdentifier = isRTL ? "Left" : "Right"; let collapseKeyIdentifier = isRTL ? "Right" : "Left"; var handled = false; var nextSelectedElement; if (this.selectedTreeElement) { if (event.keyIdentifier === collapseKeyIdentifier) { if (this.selectedTreeElement.expanded) { if (event.altKey) this.selectedTreeElement.collapseRecursively(); else this.selectedTreeElement.collapse(); handled = true; } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) { handled = true; if (this.selectedTreeElement.parent.selectable) { nextSelectedElement = this.selectedTreeElement.parent; while (nextSelectedElement && !nextSelectedElement.selectable) nextSelectedElement = nextSelectedElement.parent; handled = nextSelectedElement ? true : false; } else if (this.selectedTreeElement.parent) this.selectedTreeElement.parent.collapse(); } } else if (event.keyIdentifier === expandKeyIdentifier) { if (!this.selectedTreeElement.revealed()) { this.selectedTreeElement.reveal(); handled = true; } else if (this.selectedTreeElement.expandable) { handled = true; if (this.selectedTreeElement.expanded) { nextSelectedElement = this.selectedTreeElement.children[0]; while (nextSelectedElement && !nextSelectedElement.selectable) nextSelectedElement = nextSelectedElement.nextSibling; handled = nextSelectedElement ? true : false; } else { if (event.altKey) this.selectedTreeElement.expandRecursively(); else this.selectedTreeElement.expand(); } } } else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */) { for (let treeElement of this.selectedTreeElements) { if (treeElement.ondelete && treeElement.ondelete()) handled = true; } if (!handled && this.treeOutline.ondelete) handled = this.treeOutline.ondelete(this.selectedTreeElement); } else if (isEnterKey(event)) { if (this.selectedTreeElement.onenter) handled = this.selectedTreeElement.onenter(); if (!handled && this.treeOutline.onenter) handled = this.treeOutline.onenter(this.selectedTreeElement); } else if (event.keyIdentifier === "U+0020" /* Space */) { if (this.selectedTreeElement.onspace) handled = this.selectedTreeElement.onspace(); if (!handled && this.treeOutline.onspace) handled = this.treeOutline.onspace(this.selectedTreeElement); } } if (!handled) { this._itemWasSelectedByUser = true; handled = this._selectionController.handleKeyDown(event); this._itemWasSelectedByUser = false; if (handled) nextSelectedElement = this.selectedTreeElement; } if (nextSelectedElement) { nextSelectedElement.reveal(); nextSelectedElement.select(false, true); } if (handled) { event.preventDefault(); event.stopPropagation(); } } expand() { // this is the root, do nothing } collapse() { // this is the root, do nothing } revealed() { return true; } reveal() { // this is the root, do nothing } select() { // this is the root, do nothing } revealAndSelect(omitFocus) { // this is the root, do nothing } selectTreeElements(treeElements) { if (!treeElements.length) return; if (treeElements.length === 1) { this.selectedTreeElement = treeElements[0]; return; } console.assert(this.allowsMultipleSelection, "Cannot select TreeElements with multiple selection disabled."); if (!this.allowsMultipleSelection) return; let selectableObjects = treeElements.map((treeElement) => this.objectForSelection(treeElement)); this._selectionController.selectItems(new Set(selectableObjects)); } get virtualized() { return this._virtualizedScrollContainer && !isNaN(this._virtualizedTreeItemHeight); } registerScrollVirtualizer(scrollContainer, treeItemHeight) { console.assert(scrollContainer); console.assert(!isNaN(treeItemHeight)); console.assert(!this.virtualized); let boundUpdateVirtualizedElements = (focusedTreeElement) => { this._updateVirtualizedElements(focusedTreeElement); }; this._virtualizedDebouncer = new Debouncer(boundUpdateVirtualizedElements); this._virtualizedVisibleTreeElements = new Set; this._virtualizedAttachedTreeElements = new Set; this._virtualizedScrollContainer = scrollContainer; this._virtualizedTreeItemHeight = treeItemHeight; this._virtualizedTopSpacer = document.createElement("div"); this._virtualizedBottomSpacer = document.createElement("div"); let throttler = new Throttler(boundUpdateVirtualizedElements, 1000 / 16); this._virtualizedScrollContainer.addEventListener("scroll", (event) => { throttler.fire(); }); this._updateVirtualizedElements(); } get updateVirtualizedElementsDebouncer() { return this._virtualizedDebouncer; } // SelectionController delegate selectionControllerSelectionDidChange(controller, deselectedItems, selectedItems) { this._processingSelectionChange = true; for (let representedObject of deselectedItems) { let treeElement = this.getCachedTreeElement(representedObject); if (!treeElement) continue; if (treeElement.listItemElement) treeElement.listItemElement.classList.remove("selected"); treeElement.deselect(); } for (let representedObject of selectedItems) { let treeElement = this.getCachedTreeElement(representedObject); if (!treeElement) continue; if (treeElement.listItemElement) treeElement.listItemElement.classList.add("selected"); const omitFocus = true; treeElement.select(omitFocus); } this._dispatchSelectionDidChangeEvent(); this._processingSelectionChange = false; } selectionControllerFirstSelectableItem(controller) { let firstChild = this.children[0]; if (firstChild.selectable) return firstChild.representedObject; return this.selectionControllerNextSelectableItem(controller, firstChild.representedObject); } selectionControllerLastSelectableItem(controller) { let treeElement = this.children.lastValue; while (treeElement.expanded && treeElement.children.length) treeElement = treeElement.children.lastValue; let item = this.objectForSelection(treeElement); if (this.canSelectTreeElement(treeElement)) return item; return this.selectionControllerPreviousSelectableItem(controller, item); } selectionControllerPreviousSelectableItem(controller, item) { let treeElement = this.getCachedTreeElement(item); console.assert(treeElement, "Missing TreeElement for representedObject.", item); if (!treeElement) return null; const skipUnrevealed = true; const stayWithin = null; const dontPopulate = true; while (treeElement = treeElement.traversePreviousTreeElement(skipUnrevealed, stayWithin, dontPopulate)) { if (this.canSelectTreeElement(treeElement)) return this.objectForSelection(treeElement); } return null; } selectionControllerNextSelectableItem(controller, item) { let treeElement = this.getCachedTreeElement(item); console.assert(treeElement, "Missing TreeElement for representedObject.", item); if (!treeElement) return null; const skipUnrevealed = true; const stayWithin = null; const dontPopulate = true; while (treeElement = treeElement.traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate)) { if (this.canSelectTreeElement(treeElement)) return this.objectForSelection(treeElement); } return null; } // Protected canSelectTreeElement(treeElement) { // Can be overridden by subclasses. return treeElement.selectable; } objectForSelection(treeElement) { return treeElement.representedObject; } selectTreeElementInternal(treeElement, suppressNotification = false, selectedByUser = false) { if (this._processingSelectionChange) return; this._itemWasSelectedByUser = selectedByUser; this._suppressNextSelectionDidChangeEvent = suppressNotification; if (this.allowsRepeatSelection && this.selectedTreeElement === treeElement) { this._dispatchSelectionDidChangeEvent(); return; } this.selectedTreeElement = treeElement; } treeElementFromEvent(event) { // We can't take event.pageX to be our X coordinate, since the TreeElement // could be indented, in which case we can't rely on its DOM element to be // under the mouse. // We choose this X coordinate based on the knowledge that our list // items extend at least to the trailing edge of the outer
    container. // In the no-word-wrap mode the outer
      may be wider than the tree container // (and partially hidden), in which case we use the edge of its container. let scrollContainer = this.element.parentElement; if (scrollContainer.offsetWidth > this.element.offsetWidth) scrollContainer = this.element; // This adjustment is useful in order to find the inner-most tree element that // lines up horizontally with the location of the event. If the mouse event // happened in the space preceding a nested tree element (in the leading indentated // space) we use this adjustment to get the nested tree element and not a tree element // from a parent / outer tree outline / tree element. // // NOTE: This can fail if there is floating content over the trailing edge of // the
    1. content, since the element from point could hit that. let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL; let trailingEdgeOffset = isRTL ? 36 : (scrollContainer.offsetWidth - 36); let x = scrollContainer.totalOffsetLeft + trailingEdgeOffset; let y = event.pageY; // Our list items have 1-pixel cracks between them vertically. We avoid // the cracks by checking slightly above and slightly below the mouse // and seeing if we hit the same element each time. let elementUnderMouse = this.treeElementFromPoint(x, y); let elementAboveMouse = this.treeElementFromPoint(x, y - 2); let element = null; if (elementUnderMouse === elementAboveMouse) element = elementUnderMouse; else element = this.treeElementFromPoint(x, y + 2); return element; } populateContextMenu(contextMenu, event, treeElement) { treeElement.populateContextMenu(contextMenu, event); } // Private static _generateStyleRulesIfNeeded() { if (WI.TreeOutline._styleElement) return; WI.TreeOutline._styleElement = document.createElement("style"); let maximumTreeDepth = 32; let depthPadding = 10; let styleText = ""; let childrenSubstring = ""; for (let i = 1; i <= maximumTreeDepth; ++i) { // Keep all the elements at the same depth once the maximum is reached. childrenSubstring += i === maximumTreeDepth ? " .children" : " > .children"; styleText += `.${WI.TreeOutline.ElementStyleClassName}:not(.${WI.TreeOutline.CustomIndentStyleClassName})${childrenSubstring} > .item { `; styleText += `padding-inline-start: calc(var(--tree-outline-item-padding) + ${depthPadding * i}px);`; styleText += ` }\n`; } WI.TreeOutline._styleElement.textContent = styleText; document.head.appendChild(WI.TreeOutline._styleElement); } _updateVirtualizedElements(focusedTreeElement) { console.assert(this.virtualized); this._virtualizedDebouncer.cancel(); function walk(parent, callback, count = 0) { let shouldReturn = false; for (let child of parent.children) { if (!child.revealed(false)) continue; shouldReturn = callback(child, count); if (shouldReturn) break; ++count; if (child.expanded) { let result = walk(child, callback, count); count = result.count; if (result.shouldReturn) break; } } return {count, shouldReturn}; } function calculateOffsetFromContainer(node, target) { let top = 0; while (node !== target) { top += node.offsetTop; node = node.offsetParent; if (!node) return 0; } return top; } let offsetFromContainer = calculateOffsetFromContainer(this._virtualizedTopSpacer.parentNode ? this._virtualizedTopSpacer : this.element, this._virtualizedScrollContainer); let numberVisible = Math.ceil(Math.max(0, this._virtualizedScrollContainer.offsetHeight - offsetFromContainer) / this._virtualizedTreeItemHeight); let extraRows = Math.max(numberVisible * 5, 50); let firstItem = Math.floor((this._virtualizedScrollContainer.scrollTop - offsetFromContainer) / this._virtualizedTreeItemHeight) - extraRows; let lastItem = firstItem + numberVisible + (extraRows * 2); let shouldScroll = false; if (focusedTreeElement && focusedTreeElement.revealed(false)) { let index = walk(this, (treeElement) => treeElement === focusedTreeElement).count; if (index < firstItem) { firstItem = index - extraRows; lastItem = index + numberVisible + extraRows; } else if (index > lastItem) { firstItem = index - numberVisible - extraRows; lastItem = index + extraRows; } // Only scroll if the `focusedTreeElement` is outside the visible items, not including // the added buffer `extraRows`. shouldScroll = (index < firstItem + extraRows) || (index > lastItem - extraRows); } console.assert(firstItem < lastItem); let visibleTreeElements = new Set; let treeElementsToAttach = new Set; let treeElementsToDetach = new Set; let totalItems = walk(this, (treeElement, count) => { if (count >= firstItem && count <= lastItem) { treeElementsToAttach.add(treeElement); if (count >= firstItem + extraRows && count <= lastItem - extraRows) visibleTreeElements.add(treeElement); } else if (treeElement._listItemNode.parentNode) treeElementsToDetach.add(treeElement); return false; }).count; // Redraw if we are about to scroll. if (!shouldScroll) { // Redraw if there are a different number of items to show. if (visibleTreeElements.size === this._virtualizedVisibleTreeElements.size) { // Redraw if all of the previously centered `WI.TreeElement` are no longer centered. if (visibleTreeElements.intersects(this._virtualizedVisibleTreeElements)) { // Redraw if there is a `WI.TreeElement` that should be shown that isn't attached. if (visibleTreeElements.isSubsetOf(this._virtualizedAttachedTreeElements)) return; } } } this._virtualizedVisibleTreeElements = visibleTreeElements; this._virtualizedAttachedTreeElements = treeElementsToAttach; for (let treeElement of treeElementsToDetach) treeElement._listItemNode.remove(); for (let treeElement of treeElementsToAttach) { treeElement.parent._childrenListNode.appendChild(treeElement._listItemNode); if (treeElement._childrenListNode) treeElement.parent._childrenListNode.appendChild(treeElement._childrenListNode); } this._virtualizedTopSpacer.style.height = (Number.constrain(firstItem, 0, totalItems) * this._virtualizedTreeItemHeight) + "px"; if (this.element.previousElementSibling !== this._virtualizedTopSpacer) this.element.parentNode.insertBefore(this._virtualizedTopSpacer, this.element); this._virtualizedBottomSpacer.style.height = (Number.constrain(totalItems - lastItem, 0, totalItems) * this._virtualizedTreeItemHeight) + "px"; if (this.element.nextElementSibling !== this._virtualizedBottomSpacer) this.element.parentNode.insertBefore(this._virtualizedBottomSpacer, this.element.nextElementSibling); if (shouldScroll) this._virtualizedScrollContainer.scrollTop = offsetFromContainer + ((firstItem + extraRows) * this._virtualizedTreeItemHeight); } _handleContextmenu(event) { let treeElement = this.treeElementFromEvent(event); if (!treeElement) return; let contextMenu = WI.ContextMenu.createFromEvent(event); this.populateContextMenu(contextMenu, event, treeElement); } _handleMouseDown(event) { let treeElement = this.treeElementFromEvent(event); if (!treeElement || !treeElement.selectable) return; if (treeElement.isEventWithinDisclosureTriangle(event)) { event.preventDefault(); return; } if (!treeElement.canSelectOnMouseDown(event)) return; if (this.allowsRepeatSelection && treeElement.selected && this._selectionController.selectedItems.size === 1) { // Special case for dispatching a selection event for an already selected // item in single-selection mode. this._itemWasSelectedByUser = true; this._dispatchSelectionDidChangeEvent(); return; } this._itemWasSelectedByUser = true; this._selectionController.handleItemMouseDown(this.objectForSelection(treeElement), event); this._itemWasSelectedByUser = false; treeElement.focus(); } _dispatchSelectionDidChangeEvent() { let selectedByUser = this._itemWasSelectedByUser; this._itemWasSelectedByUser = false; if (this._suppressNextSelectionDidChangeEvent) { this._suppressNextSelectionDidChangeEvent = false; return; } this.dispatchEventToListeners(WI.TreeOutline.Event.SelectionDidChange, {selectedByUser}); } }; WI.TreeOutline._styleElement = null; WI.TreeOutline.ElementStyleClassName = "tree-outline"; WI.TreeOutline.CustomIndentStyleClassName = "custom-indent"; WI.TreeOutline.Event = { ElementAdded: "element-added", ElementDidChange: "element-did-change", ElementRemoved: "element-removed", ElementRevealed: "element-revealed", ElementClicked: "element-clicked", ElementDisclosureDidChanged: "element-disclosure-did-change", ElementVisibilityDidChange: "element-visbility-did-change", SelectionDidChange: "selection-did-change", }; WI.TreeOutline._knownTreeElementNextIdentifier = 1;