|  | /* | 
|  | * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved. | 
|  | * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> | 
|  | * Copyright (C) 2009 Joseph Pecoraro | 
|  | * | 
|  | * 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 | 
|  | */ | 
|  | Elements.ElementsTreeOutline = class extends UI.TreeOutline { | 
|  | /** | 
|  | * @param {boolean=} omitRootDOMNode | 
|  | * @param {boolean=} selectEnabled | 
|  | */ | 
|  | constructor(omitRootDOMNode, selectEnabled) { | 
|  | super(); | 
|  |  | 
|  | this._treeElementSymbol = Symbol('treeElement'); | 
|  | const shadowContainer = createElement('div'); | 
|  | this._shadowRoot = UI.createShadowRootWithCoreStyles(shadowContainer, 'elements/elementsTreeOutline.css'); | 
|  | const outlineDisclosureElement = this._shadowRoot.createChild('div', 'elements-disclosure'); | 
|  |  | 
|  | this._element = this.element; | 
|  | this._element.classList.add('elements-tree-outline', 'source-code'); | 
|  | UI.ARIAUtils.setAccessibleName(this._element, Common.UIString('Page DOM')); | 
|  | this._element.addEventListener('mousedown', this._onmousedown.bind(this), false); | 
|  | this._element.addEventListener('mousemove', this._onmousemove.bind(this), false); | 
|  | this._element.addEventListener('mouseleave', this._onmouseleave.bind(this), false); | 
|  | this._element.addEventListener('dragstart', this._ondragstart.bind(this), false); | 
|  | this._element.addEventListener('dragover', this._ondragover.bind(this), false); | 
|  | this._element.addEventListener('dragleave', this._ondragleave.bind(this), false); | 
|  | this._element.addEventListener('drop', this._ondrop.bind(this), false); | 
|  | this._element.addEventListener('dragend', this._ondragend.bind(this), false); | 
|  | this._element.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false); | 
|  | this._element.addEventListener('clipboard-beforecopy', this._onBeforeCopy.bind(this), false); | 
|  | this._element.addEventListener('clipboard-copy', this._onCopyOrCut.bind(this, false), false); | 
|  | this._element.addEventListener('clipboard-cut', this._onCopyOrCut.bind(this, true), false); | 
|  | this._element.addEventListener('clipboard-paste', this._onPaste.bind(this), false); | 
|  |  | 
|  | outlineDisclosureElement.appendChild(this._element); | 
|  | this.element = shadowContainer; | 
|  |  | 
|  | this._includeRootDOMNode = !omitRootDOMNode; | 
|  | this._selectEnabled = selectEnabled; | 
|  | /** @type {?SDK.DOMNode} */ | 
|  | this._rootDOMNode = null; | 
|  | /** @type {?SDK.DOMNode} */ | 
|  | this._selectedDOMNode = null; | 
|  |  | 
|  | this._visible = false; | 
|  |  | 
|  | this._popoverHelper = new UI.PopoverHelper(this._element, this._getPopoverRequest.bind(this)); | 
|  | this._popoverHelper.setHasPadding(true); | 
|  | this._popoverHelper.setTimeout(0, 100); | 
|  |  | 
|  | /** @type {!Map<!SDK.DOMNode, !Elements.ElementsTreeOutline.UpdateRecord>} */ | 
|  | this._updateRecords = new Map(); | 
|  | /** @type {!Set<!Elements.ElementsTreeElement>} */ | 
|  | this._treeElementsBeingUpdated = new Set(); | 
|  |  | 
|  | this._showHTMLCommentsSetting = Common.moduleSetting('showHTMLComments'); | 
|  | this._showHTMLCommentsSetting.addChangeListener(this._onShowHTMLCommentsChange.bind(this)); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMModel} domModel | 
|  | * @return {?Elements.ElementsTreeOutline} | 
|  | */ | 
|  | static forDOMModel(domModel) { | 
|  | return domModel[Elements.ElementsTreeOutline._treeOutlineSymbol] || null; | 
|  | } | 
|  |  | 
|  | _onShowHTMLCommentsChange() { | 
|  | const selectedNode = this.selectedDOMNode(); | 
|  | if (selectedNode && selectedNode.nodeType() === Node.COMMENT_NODE && !this._showHTMLCommentsSetting.get()) | 
|  | this.selectDOMNode(selectedNode.parentNode); | 
|  | this.update(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {symbol} | 
|  | */ | 
|  | treeElementSymbol() { | 
|  | return this._treeElementSymbol; | 
|  | } | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {boolean} wrap | 
|  | */ | 
|  | setWordWrap(wrap) { | 
|  | this._element.classList.toggle('elements-tree-nowrap', !wrap); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {?Elements.MultilineEditorController} multilineEditing | 
|  | */ | 
|  | setMultilineEditing(multilineEditing) { | 
|  | this._multilineEditing = multilineEditing; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {number} | 
|  | */ | 
|  | visibleWidth() { | 
|  | return this._visibleWidth; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {number} width | 
|  | */ | 
|  | setVisibleWidth(width) { | 
|  | this._visibleWidth = width; | 
|  | if (this._multilineEditing) | 
|  | this._multilineEditing.resize(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {?Elements.ElementsTreeOutline.ClipboardData} data | 
|  | */ | 
|  | _setClipboardData(data) { | 
|  | if (this._clipboardNodeData) { | 
|  | const treeElement = this.findTreeElement(this._clipboardNodeData.node); | 
|  | if (treeElement) | 
|  | treeElement.setInClipboard(false); | 
|  | delete this._clipboardNodeData; | 
|  | } | 
|  |  | 
|  | if (data) { | 
|  | const treeElement = this.findTreeElement(data.node); | 
|  | if (treeElement) | 
|  | treeElement.setInClipboard(true); | 
|  | this._clipboardNodeData = data; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} removedNode | 
|  | */ | 
|  | resetClipboardIfNeeded(removedNode) { | 
|  | if (this._clipboardNodeData && this._clipboardNodeData.node === removedNode) | 
|  | this._setClipboardData(null); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Event} event | 
|  | */ | 
|  | _onBeforeCopy(event) { | 
|  | event.handled = true; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {boolean} isCut | 
|  | * @param {!Event} event | 
|  | */ | 
|  | _onCopyOrCut(isCut, event) { | 
|  | this._setClipboardData(null); | 
|  | const originalEvent = event['original']; | 
|  |  | 
|  | // Don't prevent the normal copy if the user has a selection. | 
|  | if (originalEvent.target.hasSelection()) | 
|  | return; | 
|  |  | 
|  | // Do not interfere with text editing. | 
|  | if (UI.isEditing()) | 
|  | return; | 
|  |  | 
|  | const targetNode = this.selectedDOMNode(); | 
|  | if (!targetNode) | 
|  | return; | 
|  |  | 
|  | originalEvent.clipboardData.clearData(); | 
|  | event.handled = true; | 
|  |  | 
|  | this.performCopyOrCut(isCut, targetNode); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {boolean} isCut | 
|  | * @param {?SDK.DOMNode} node | 
|  | */ | 
|  | performCopyOrCut(isCut, node) { | 
|  | if (isCut && (node.isShadowRoot() || node.ancestorUserAgentShadowRoot())) | 
|  | return; | 
|  |  | 
|  | node.copyNode(); | 
|  | this._setClipboardData({node: node, isCut: isCut}); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} targetNode | 
|  | * @return {boolean} | 
|  | */ | 
|  | canPaste(targetNode) { | 
|  | if (targetNode.isShadowRoot() || targetNode.ancestorUserAgentShadowRoot()) | 
|  | return false; | 
|  |  | 
|  | if (!this._clipboardNodeData) | 
|  | return false; | 
|  |  | 
|  | const node = this._clipboardNodeData.node; | 
|  | if (this._clipboardNodeData.isCut && (node === targetNode || node.isAncestor(targetNode))) | 
|  | return false; | 
|  |  | 
|  | if (targetNode.domModel() !== node.domModel()) | 
|  | return false; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} targetNode | 
|  | */ | 
|  | pasteNode(targetNode) { | 
|  | if (this.canPaste(targetNode)) | 
|  | this._performPaste(targetNode); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Event} event | 
|  | */ | 
|  | _onPaste(event) { | 
|  | // Do not interfere with text editing. | 
|  | if (UI.isEditing()) | 
|  | return; | 
|  |  | 
|  | const targetNode = this.selectedDOMNode(); | 
|  | if (!targetNode || !this.canPaste(targetNode)) | 
|  | return; | 
|  |  | 
|  | event.handled = true; | 
|  | this._performPaste(targetNode); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} targetNode | 
|  | */ | 
|  | _performPaste(targetNode) { | 
|  | if (this._clipboardNodeData.isCut) { | 
|  | this._clipboardNodeData.node.moveTo(targetNode, null, expandCallback.bind(this)); | 
|  | this._setClipboardData(null); | 
|  | } else { | 
|  | this._clipboardNodeData.node.copyTo(targetNode, null, expandCallback.bind(this)); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {?Protocol.Error} error | 
|  | * @param {!Protocol.DOM.NodeId} nodeId | 
|  | * @this {Elements.ElementsTreeOutline} | 
|  | */ | 
|  | function expandCallback(error, nodeId) { | 
|  | if (error) | 
|  | return; | 
|  | const pastedNode = targetNode.domModel().nodeForId(nodeId); | 
|  | if (!pastedNode) | 
|  | return; | 
|  | this.selectDOMNode(pastedNode); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {boolean} visible | 
|  | */ | 
|  | setVisible(visible) { | 
|  | this._visible = visible; | 
|  | if (!this._visible) { | 
|  | this._popoverHelper.hidePopover(); | 
|  | if (this._multilineEditing) | 
|  | this._multilineEditing.cancel(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | this.runPendingUpdates(); | 
|  | if (this._selectedDOMNode) | 
|  | this._revealAndSelectNode(this._selectedDOMNode, false); | 
|  | } | 
|  |  | 
|  | get rootDOMNode() { | 
|  | return this._rootDOMNode; | 
|  | } | 
|  |  | 
|  | set rootDOMNode(x) { | 
|  | if (this._rootDOMNode === x) | 
|  | return; | 
|  |  | 
|  | this._rootDOMNode = x; | 
|  |  | 
|  | this._isXMLMimeType = x && x.isXMLNode(); | 
|  |  | 
|  | this.update(); | 
|  | } | 
|  |  | 
|  | get isXMLMimeType() { | 
|  | return this._isXMLMimeType; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {?SDK.DOMNode} | 
|  | */ | 
|  | selectedDOMNode() { | 
|  | return this._selectedDOMNode; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {?SDK.DOMNode} node | 
|  | * @param {boolean=} focus | 
|  | */ | 
|  | selectDOMNode(node, focus) { | 
|  | if (this._selectedDOMNode === node) { | 
|  | this._revealAndSelectNode(node, !focus); | 
|  | return; | 
|  | } | 
|  |  | 
|  | this._selectedDOMNode = node; | 
|  | this._revealAndSelectNode(node, !focus); | 
|  |  | 
|  | // The _revealAndSelectNode() method might find a different element if there is inlined text, | 
|  | // and the select() call would change the selectedDOMNode and reenter this setter. So to | 
|  | // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same | 
|  | // node as the one passed in. | 
|  | if (this._selectedDOMNode === node) | 
|  | this._selectedNodeChanged(!!focus); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {boolean} | 
|  | */ | 
|  | editing() { | 
|  | const node = this.selectedDOMNode(); | 
|  | if (!node) | 
|  | return false; | 
|  | const treeElement = this.findTreeElement(node); | 
|  | if (!treeElement) | 
|  | return false; | 
|  | return treeElement.isEditing() || false; | 
|  | } | 
|  |  | 
|  | update() { | 
|  | const selectedNode = this.selectedDOMNode(); | 
|  | this.removeChildren(); | 
|  | if (!this.rootDOMNode) | 
|  | return; | 
|  |  | 
|  | if (this._includeRootDOMNode) { | 
|  | const treeElement = this._createElementTreeElement(this.rootDOMNode); | 
|  | this.appendChild(treeElement); | 
|  | } else { | 
|  | // FIXME: this could use findTreeElement to reuse a tree element if it already exists | 
|  | const children = this._visibleChildren(this.rootDOMNode); | 
|  | for (const child of children) { | 
|  | const treeElement = this._createElementTreeElement(child); | 
|  | this.appendChild(treeElement); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (selectedNode) | 
|  | this._revealAndSelectNode(selectedNode, true); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {boolean} focus | 
|  | */ | 
|  | _selectedNodeChanged(focus) { | 
|  | this.dispatchEventToListeners( | 
|  | Elements.ElementsTreeOutline.Events.SelectedNodeChanged, {node: this._selectedDOMNode, focus: focus}); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Array.<!SDK.DOMNode>} nodes | 
|  | */ | 
|  | _fireElementsTreeUpdated(nodes) { | 
|  | this.dispatchEventToListeners(Elements.ElementsTreeOutline.Events.ElementsTreeUpdated, nodes); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @return {?Elements.ElementsTreeElement} | 
|  | */ | 
|  | findTreeElement(node) { | 
|  | let treeElement = this._lookUpTreeElement(node); | 
|  | if (!treeElement && node.nodeType() === Node.TEXT_NODE) { | 
|  | // The text node might have been inlined if it was short, so try to find the parent element. | 
|  | treeElement = this._lookUpTreeElement(node.parentNode); | 
|  | } | 
|  |  | 
|  | return /** @type {?Elements.ElementsTreeElement} */ (treeElement); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {?SDK.DOMNode} node | 
|  | * @return {?UI.TreeElement} | 
|  | */ | 
|  | _lookUpTreeElement(node) { | 
|  | if (!node) | 
|  | return null; | 
|  |  | 
|  | const cachedElement = node[this._treeElementSymbol]; | 
|  | if (cachedElement) | 
|  | return cachedElement; | 
|  |  | 
|  | // Walk up the parent pointers from the desired node | 
|  | const ancestors = []; | 
|  | let currentNode; | 
|  | for (currentNode = node.parentNode; currentNode; currentNode = currentNode.parentNode) { | 
|  | ancestors.push(currentNode); | 
|  | if (currentNode[this._treeElementSymbol])  // stop climbing as soon as we hit | 
|  | break; | 
|  | } | 
|  |  | 
|  | if (!currentNode) | 
|  | return null; | 
|  |  | 
|  | // Walk down to populate each ancestor's children, to fill in the tree and the cache. | 
|  | for (let i = ancestors.length - 1; i >= 0; --i) { | 
|  | const treeElement = ancestors[i][this._treeElementSymbol]; | 
|  | if (treeElement) | 
|  | treeElement.onpopulate();  // fill the cache with the children of treeElement | 
|  | } | 
|  |  | 
|  | return node[this._treeElementSymbol]; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @return {?Elements.ElementsTreeElement} | 
|  | */ | 
|  | createTreeElementFor(node) { | 
|  | let treeElement = this.findTreeElement(node); | 
|  | if (treeElement) | 
|  | return treeElement; | 
|  | if (!node.parentNode) | 
|  | return null; | 
|  |  | 
|  | treeElement = this.createTreeElementFor(node.parentNode); | 
|  | return treeElement ? this._showChild(treeElement, node) : null; | 
|  | } | 
|  |  | 
|  | set suppressRevealAndSelect(x) { | 
|  | if (this._suppressRevealAndSelect === x) | 
|  | return; | 
|  | this._suppressRevealAndSelect = x; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {?SDK.DOMNode} node | 
|  | * @param {boolean} omitFocus | 
|  | */ | 
|  | _revealAndSelectNode(node, omitFocus) { | 
|  | if (this._suppressRevealAndSelect) | 
|  | return; | 
|  |  | 
|  | if (!this._includeRootDOMNode && node === this.rootDOMNode && this.rootDOMNode) | 
|  | node = this.rootDOMNode.firstChild; | 
|  | if (!node) | 
|  | return; | 
|  | const treeElement = this.createTreeElementFor(node); | 
|  | if (!treeElement) | 
|  | return; | 
|  |  | 
|  | treeElement.revealAndSelect(omitFocus); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {?UI.TreeElement} | 
|  | */ | 
|  | _treeElementFromEvent(event) { | 
|  | const scrollContainer = this.element.parentElement; | 
|  |  | 
|  | // We choose this X coordinate based on the knowledge that our list | 
|  | // items extend at least to the right edge of the outer <ol> container. | 
|  | // In the no-word-wrap mode the outer <ol> may be wider than the tree container | 
|  | // (and partially hidden), in which case we are left to use only its right boundary. | 
|  | const x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36; | 
|  |  | 
|  | const 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. | 
|  | const elementUnderMouse = this.treeElementFromPoint(x, y); | 
|  | const elementAboveMouse = this.treeElementFromPoint(x, y - 2); | 
|  | let element; | 
|  | if (elementUnderMouse === elementAboveMouse) | 
|  | element = elementUnderMouse; | 
|  | else | 
|  | element = this.treeElementFromPoint(x, y + 2); | 
|  |  | 
|  | return element; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Event} event | 
|  | * @return {?UI.PopoverRequest} | 
|  | */ | 
|  | _getPopoverRequest(event) { | 
|  | let link = event.target; | 
|  | while (link && !link[Elements.ElementsTreeElement.HrefSymbol]) | 
|  | link = link.parentElementOrShadowHost(); | 
|  | if (!link) | 
|  | return null; | 
|  |  | 
|  | return { | 
|  | box: link.boxInWindow(), | 
|  | show: async popover => { | 
|  | const listItem = link.enclosingNodeOrSelfWithNodeName('li'); | 
|  | const node = /** @type {!Elements.ElementsTreeElement} */ (listItem.treeElement).node(); | 
|  | const precomputedFeatures = await this._loadDimensionsForNode(node); | 
|  | const preview = await BrowserComponents.ImagePreview.build( | 
|  | node.domModel().target(), link[Elements.ElementsTreeElement.HrefSymbol], true, precomputedFeatures); | 
|  | if (preview) | 
|  | popover.contentElement.appendChild(preview); | 
|  | return !!preview; | 
|  | } | 
|  | }; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @return {!Promise<!Object|undefined>} | 
|  | */ | 
|  | async _loadDimensionsForNode(node) { | 
|  | if (!node.nodeName() || node.nodeName().toLowerCase() !== 'img') | 
|  | return; | 
|  |  | 
|  | const object = await node.resolveToObject(''); | 
|  |  | 
|  | if (!object) | 
|  | return; | 
|  |  | 
|  | const promise = object.callFunctionJSONPromise(features, undefined); | 
|  | object.release(); | 
|  | return promise; | 
|  |  | 
|  | /** | 
|  | * @return {!{offsetWidth: number, offsetHeight: number, naturalWidth: number, naturalHeight: number, currentSrc: (string|undefined)}} | 
|  | * @suppressReceiverCheck | 
|  | * @this {!Element} | 
|  | */ | 
|  | function features() { | 
|  | return { | 
|  | offsetWidth: this.offsetWidth, | 
|  | offsetHeight: this.offsetHeight, | 
|  | naturalWidth: this.naturalWidth, | 
|  | naturalHeight: this.naturalHeight, | 
|  | currentSrc: this.currentSrc | 
|  | }; | 
|  | } | 
|  | } | 
|  |  | 
|  | _onmousedown(event) { | 
|  | const element = this._treeElementFromEvent(event); | 
|  |  | 
|  | if (!element || element.isEventWithinDisclosureTriangle(event)) | 
|  | return; | 
|  |  | 
|  | element.select(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {?UI.TreeElement} treeElement | 
|  | */ | 
|  | setHoverEffect(treeElement) { | 
|  | if (this._previousHoveredElement === treeElement) | 
|  | return; | 
|  |  | 
|  | if (this._previousHoveredElement) { | 
|  | this._previousHoveredElement.hovered = false; | 
|  | delete this._previousHoveredElement; | 
|  | } | 
|  |  | 
|  | if (treeElement) { | 
|  | treeElement.hovered = true; | 
|  | this._previousHoveredElement = treeElement; | 
|  | } | 
|  | } | 
|  |  | 
|  | _onmousemove(event) { | 
|  | const element = this._treeElementFromEvent(event); | 
|  | if (element && this._previousHoveredElement === element) | 
|  | return; | 
|  |  | 
|  | this.setHoverEffect(element); | 
|  |  | 
|  | if (element instanceof Elements.ElementsTreeElement) { | 
|  | element.node().domModel().overlayModel().highlightDOMNodeWithConfig( | 
|  | element.node().id, {mode: 'all', showInfo: !UI.KeyboardShortcut.eventHasCtrlOrMeta(event)}); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (element instanceof Elements.ElementsTreeOutline.ShortcutTreeElement) { | 
|  | element.domModel().overlayModel().highlightDOMNodeWithConfig( | 
|  | undefined, {mode: 'all', showInfo: !UI.KeyboardShortcut.eventHasCtrlOrMeta(event)}, element.backendNodeId()); | 
|  | } | 
|  | } | 
|  |  | 
|  | _onmouseleave(event) { | 
|  | this.setHoverEffect(null); | 
|  | SDK.OverlayModel.hideDOMNodeHighlight(); | 
|  | } | 
|  |  | 
|  | _ondragstart(event) { | 
|  | if (event.target.hasSelection()) | 
|  | return false; | 
|  | if (event.target.nodeName === 'A') | 
|  | return false; | 
|  |  | 
|  | const treeElement = this._validDragSourceOrTarget(this._treeElementFromEvent(event)); | 
|  | if (!treeElement) | 
|  | return false; | 
|  |  | 
|  | if (treeElement.node().nodeName() === 'BODY' || treeElement.node().nodeName() === 'HEAD') | 
|  | return false; | 
|  |  | 
|  | event.dataTransfer.setData('text/plain', treeElement.listItemElement.textContent.replace(/\u200b/g, '')); | 
|  | event.dataTransfer.effectAllowed = 'copyMove'; | 
|  | this._treeElementBeingDragged = treeElement; | 
|  |  | 
|  | SDK.OverlayModel.hideDOMNodeHighlight(); | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | _ondragover(event) { | 
|  | if (!this._treeElementBeingDragged) | 
|  | return false; | 
|  |  | 
|  | const treeElement = this._validDragSourceOrTarget(this._treeElementFromEvent(event)); | 
|  | if (!treeElement) | 
|  | return false; | 
|  |  | 
|  | let node = treeElement.node(); | 
|  | while (node) { | 
|  | if (node === this._treeElementBeingDragged._node) | 
|  | return false; | 
|  | node = node.parentNode; | 
|  | } | 
|  |  | 
|  | treeElement.listItemElement.classList.add('elements-drag-over'); | 
|  | this._dragOverTreeElement = treeElement; | 
|  | event.preventDefault(); | 
|  | event.dataTransfer.dropEffect = 'move'; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | _ondragleave(event) { | 
|  | this._clearDragOverTreeElementMarker(); | 
|  | event.preventDefault(); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {?UI.TreeElement} treeElement | 
|  | * @return {?Elements.ElementsTreeElement} | 
|  | */ | 
|  | _validDragSourceOrTarget(treeElement) { | 
|  | if (!treeElement) | 
|  | return null; | 
|  |  | 
|  | if (!(treeElement instanceof Elements.ElementsTreeElement)) | 
|  | return null; | 
|  | const elementsTreeElement = /** @type {!Elements.ElementsTreeElement} */ (treeElement); | 
|  |  | 
|  | const node = elementsTreeElement.node(); | 
|  | if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE) | 
|  | return null; | 
|  |  | 
|  | return elementsTreeElement; | 
|  | } | 
|  |  | 
|  | _ondrop(event) { | 
|  | event.preventDefault(); | 
|  | const treeElement = this._treeElementFromEvent(event); | 
|  | if (treeElement instanceof Elements.ElementsTreeElement) | 
|  | this._doMove(treeElement); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | */ | 
|  | _doMove(treeElement) { | 
|  | if (!this._treeElementBeingDragged) | 
|  | return; | 
|  |  | 
|  | let parentNode; | 
|  | let anchorNode; | 
|  |  | 
|  | if (treeElement.isClosingTag()) { | 
|  | // Drop onto closing tag -> insert as last child. | 
|  | parentNode = treeElement.node(); | 
|  | } else { | 
|  | const dragTargetNode = treeElement.node(); | 
|  | parentNode = dragTargetNode.parentNode; | 
|  | anchorNode = dragTargetNode; | 
|  | } | 
|  |  | 
|  | const wasExpanded = this._treeElementBeingDragged.expanded; | 
|  | this._treeElementBeingDragged._node.moveTo( | 
|  | parentNode, anchorNode, this.selectNodeAfterEdit.bind(this, wasExpanded)); | 
|  |  | 
|  | delete this._treeElementBeingDragged; | 
|  | } | 
|  |  | 
|  | _ondragend(event) { | 
|  | event.preventDefault(); | 
|  | this._clearDragOverTreeElementMarker(); | 
|  | delete this._treeElementBeingDragged; | 
|  | } | 
|  |  | 
|  | _clearDragOverTreeElementMarker() { | 
|  | if (this._dragOverTreeElement) { | 
|  | this._dragOverTreeElement.listItemElement.classList.remove('elements-drag-over'); | 
|  | delete this._dragOverTreeElement; | 
|  | } | 
|  | } | 
|  |  | 
|  | _contextMenuEventFired(event) { | 
|  | const treeElement = this._treeElementFromEvent(event); | 
|  | if (treeElement instanceof Elements.ElementsTreeElement) | 
|  | this.showContextMenu(treeElement, event); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | * @param {!Event} event | 
|  | */ | 
|  | showContextMenu(treeElement, event) { | 
|  | if (UI.isEditing()) | 
|  | return; | 
|  |  | 
|  | const contextMenu = new UI.ContextMenu(event); | 
|  | const isPseudoElement = !!treeElement.node().pseudoType(); | 
|  | const isTag = treeElement.node().nodeType() === Node.ELEMENT_NODE && !isPseudoElement; | 
|  | let textNode = event.target.enclosingNodeOrSelfWithClass('webkit-html-text-node'); | 
|  | if (textNode && textNode.classList.contains('bogus')) | 
|  | textNode = null; | 
|  | const commentNode = event.target.enclosingNodeOrSelfWithClass('webkit-html-comment'); | 
|  | if (textNode) | 
|  | treeElement.populateTextContextMenu(contextMenu, textNode); | 
|  | else if (isTag) | 
|  | treeElement.populateTagContextMenu(contextMenu, event); | 
|  | else if (commentNode) | 
|  | treeElement.populateNodeContextMenu(contextMenu); | 
|  | else if (isPseudoElement) | 
|  | treeElement.populateScrollIntoView(contextMenu); | 
|  |  | 
|  | contextMenu.appendApplicableItems(treeElement.node()); | 
|  | contextMenu.show(); | 
|  | } | 
|  |  | 
|  | runPendingUpdates() { | 
|  | this._updateModifiedNodes(); | 
|  | } | 
|  |  | 
|  | handleShortcut(event) { | 
|  | const node = this.selectedDOMNode(); | 
|  | if (!node) | 
|  | return; | 
|  | const treeElement = node[this._treeElementSymbol]; | 
|  | if (!treeElement) | 
|  | return; | 
|  |  | 
|  | if (UI.KeyboardShortcut.eventHasCtrlOrMeta(event) && node.parentNode) { | 
|  | if (event.key === 'ArrowUp' && node.previousSibling) { | 
|  | node.moveTo(node.parentNode, node.previousSibling, this.selectNodeAfterEdit.bind(this, treeElement.expanded)); | 
|  | event.handled = true; | 
|  | return; | 
|  | } | 
|  | if (event.key === 'ArrowDown' && node.nextSibling) { | 
|  | node.moveTo( | 
|  | node.parentNode, node.nextSibling.nextSibling, this.selectNodeAfterEdit.bind(this, treeElement.expanded)); | 
|  | event.handled = true; | 
|  | return; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @param {boolean=} startEditing | 
|  | * @param {function()=} callback | 
|  | */ | 
|  | toggleEditAsHTML(node, startEditing, callback) { | 
|  | const treeElement = node[this._treeElementSymbol]; | 
|  | if (!treeElement || !treeElement.hasEditableNode()) | 
|  | return; | 
|  |  | 
|  | if (node.pseudoType()) | 
|  | return; | 
|  |  | 
|  | const parentNode = node.parentNode; | 
|  | const index = node.index; | 
|  | const wasExpanded = treeElement.expanded; | 
|  |  | 
|  | treeElement.toggleEditAsHTML(editingFinished.bind(this), startEditing); | 
|  |  | 
|  | /** | 
|  | * @this {Elements.ElementsTreeOutline} | 
|  | * @param {boolean} success | 
|  | */ | 
|  | function editingFinished(success) { | 
|  | if (callback) | 
|  | callback(); | 
|  | if (!success) | 
|  | return; | 
|  |  | 
|  | // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date. | 
|  | this.runPendingUpdates(); | 
|  |  | 
|  | const newNode = parentNode ? parentNode.children()[index] || parentNode : null; | 
|  | if (!newNode) | 
|  | return; | 
|  |  | 
|  | this.selectDOMNode(newNode, true); | 
|  |  | 
|  | if (wasExpanded) { | 
|  | const newTreeItem = this.findTreeElement(newNode); | 
|  | if (newTreeItem) | 
|  | newTreeItem.expand(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {boolean} wasExpanded | 
|  | * @param {?Protocol.Error} error | 
|  | * @param {?SDK.DOMNode} newNode | 
|  | * @return {?Elements.ElementsTreeElement} nodeId | 
|  | */ | 
|  | selectNodeAfterEdit(wasExpanded, error, newNode) { | 
|  | if (error) | 
|  | return null; | 
|  |  | 
|  | // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date. | 
|  | this.runPendingUpdates(); | 
|  |  | 
|  | if (!newNode) | 
|  | return null; | 
|  |  | 
|  | this.selectDOMNode(newNode, true); | 
|  |  | 
|  | const newTreeItem = this.findTreeElement(newNode); | 
|  | if (wasExpanded) { | 
|  | if (newTreeItem) | 
|  | newTreeItem.expand(); | 
|  | } | 
|  | return newTreeItem; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Runs a script on the node's remote object that toggles a class name on | 
|  | * the node and injects a stylesheet into the head of the node's document | 
|  | * containing a rule to set "visibility: hidden" on the class and all it's | 
|  | * ancestors. | 
|  | * | 
|  | * @param {!SDK.DOMNode} node | 
|  | */ | 
|  | async toggleHideElement(node) { | 
|  | const pseudoType = node.pseudoType(); | 
|  | const effectiveNode = pseudoType ? node.parentNode : node; | 
|  | if (!effectiveNode) | 
|  | return; | 
|  |  | 
|  | const hidden = node.marker('hidden-marker'); | 
|  | const object = await effectiveNode.resolveToObject(''); | 
|  |  | 
|  | if (!object) | 
|  | return; | 
|  |  | 
|  | const result = object.callFunction(toggleClassAndInjectStyleRule, [{value: pseudoType}, {value: !hidden}]); | 
|  | object.release(); | 
|  | node.setMarker('hidden-marker', hidden ? null : true); | 
|  | return result; | 
|  |  | 
|  | /** | 
|  | * @param {?string} pseudoType | 
|  | * @param {boolean} hidden | 
|  | * @suppressGlobalPropertiesCheck | 
|  | * @suppressReceiverCheck | 
|  | * @this {!Element} | 
|  | */ | 
|  | function toggleClassAndInjectStyleRule(pseudoType, hidden) { | 
|  | const classNamePrefix = '__web-inspector-hide'; | 
|  | const classNameSuffix = '-shortcut__'; | 
|  | const styleTagId = '__web-inspector-hide-shortcut-style__'; | 
|  | const selectors = []; | 
|  | selectors.push('.__web-inspector-hide-shortcut__'); | 
|  | selectors.push('.__web-inspector-hide-shortcut__ *'); | 
|  | selectors.push('.__web-inspector-hidebefore-shortcut__::before'); | 
|  | selectors.push('.__web-inspector-hideafter-shortcut__::after'); | 
|  | const selector = selectors.join(', '); | 
|  | const ruleBody = '    visibility: hidden !important;'; | 
|  | const rule = '\n' + selector + '\n{\n' + ruleBody + '\n}\n'; | 
|  | const className = classNamePrefix + (pseudoType || '') + classNameSuffix; | 
|  | this.classList.toggle(className, hidden); | 
|  |  | 
|  | let localRoot = this; | 
|  | while (localRoot.parentNode) | 
|  | localRoot = localRoot.parentNode; | 
|  | if (localRoot.nodeType === Node.DOCUMENT_NODE) | 
|  | localRoot = document.head; | 
|  |  | 
|  | let style = localRoot.querySelector('style#' + styleTagId); | 
|  | if (style) | 
|  | return; | 
|  |  | 
|  | style = document.createElement('style'); | 
|  | style.id = styleTagId; | 
|  | style.type = 'text/css'; | 
|  | style.textContent = rule; | 
|  |  | 
|  | localRoot.appendChild(style); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @return {boolean} | 
|  | */ | 
|  | isToggledToHidden(node) { | 
|  | return !!node.marker('hidden-marker'); | 
|  | } | 
|  |  | 
|  | _reset() { | 
|  | this.rootDOMNode = null; | 
|  | this.selectDOMNode(null, false); | 
|  | this._popoverHelper.hidePopover(); | 
|  | delete this._clipboardNodeData; | 
|  | SDK.OverlayModel.hideDOMNodeHighlight(); | 
|  | this._updateRecords.clear(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMModel} domModel | 
|  | */ | 
|  | wireToDOMModel(domModel) { | 
|  | domModel[Elements.ElementsTreeOutline._treeOutlineSymbol] = this; | 
|  | domModel.addEventListener(SDK.DOMModel.Events.MarkersChanged, this._markersChanged, this); | 
|  | domModel.addEventListener(SDK.DOMModel.Events.NodeInserted, this._nodeInserted, this); | 
|  | domModel.addEventListener(SDK.DOMModel.Events.NodeRemoved, this._nodeRemoved, this); | 
|  | domModel.addEventListener(SDK.DOMModel.Events.AttrModified, this._attributeModified, this); | 
|  | domModel.addEventListener(SDK.DOMModel.Events.AttrRemoved, this._attributeRemoved, this); | 
|  | domModel.addEventListener(SDK.DOMModel.Events.CharacterDataModified, this._characterDataModified, this); | 
|  | domModel.addEventListener(SDK.DOMModel.Events.DocumentUpdated, this._documentUpdated, this); | 
|  | domModel.addEventListener(SDK.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this); | 
|  | domModel.addEventListener(SDK.DOMModel.Events.DistributedNodesChanged, this._distributedNodesChanged, this); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMModel} domModel | 
|  | */ | 
|  | unwireFromDOMModel(domModel) { | 
|  | domModel.removeEventListener(SDK.DOMModel.Events.MarkersChanged, this._markersChanged, this); | 
|  | domModel.removeEventListener(SDK.DOMModel.Events.NodeInserted, this._nodeInserted, this); | 
|  | domModel.removeEventListener(SDK.DOMModel.Events.NodeRemoved, this._nodeRemoved, this); | 
|  | domModel.removeEventListener(SDK.DOMModel.Events.AttrModified, this._attributeModified, this); | 
|  | domModel.removeEventListener(SDK.DOMModel.Events.AttrRemoved, this._attributeRemoved, this); | 
|  | domModel.removeEventListener(SDK.DOMModel.Events.CharacterDataModified, this._characterDataModified, this); | 
|  | domModel.removeEventListener(SDK.DOMModel.Events.DocumentUpdated, this._documentUpdated, this); | 
|  | domModel.removeEventListener(SDK.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this); | 
|  | domModel.removeEventListener(SDK.DOMModel.Events.DistributedNodesChanged, this._distributedNodesChanged, this); | 
|  | delete domModel[Elements.ElementsTreeOutline._treeOutlineSymbol]; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @return {!Elements.ElementsTreeOutline.UpdateRecord} | 
|  | */ | 
|  | _addUpdateRecord(node) { | 
|  | let record = this._updateRecords.get(node); | 
|  | if (!record) { | 
|  | record = new Elements.ElementsTreeOutline.UpdateRecord(); | 
|  | this._updateRecords.set(node, record); | 
|  | } | 
|  | return record; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @return {?Elements.ElementsTreeOutline.UpdateRecord} | 
|  | */ | 
|  | _updateRecordForHighlight(node) { | 
|  | if (!this._visible) | 
|  | return null; | 
|  | return this._updateRecords.get(node) || null; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _documentUpdated(event) { | 
|  | const domModel = /** @type {!SDK.DOMModel} */ (event.data); | 
|  | this._reset(); | 
|  | if (domModel.existingDocument()) | 
|  | this.rootDOMNode = domModel.existingDocument(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _attributeModified(event) { | 
|  | const node = /** @type {!SDK.DOMNode} */ (event.data.node); | 
|  | this._addUpdateRecord(node).attributeModified(event.data.name); | 
|  | this._updateModifiedNodesSoon(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _attributeRemoved(event) { | 
|  | const node = /** @type {!SDK.DOMNode} */ (event.data.node); | 
|  | this._addUpdateRecord(node).attributeRemoved(event.data.name); | 
|  | this._updateModifiedNodesSoon(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _characterDataModified(event) { | 
|  | const node = /** @type {!SDK.DOMNode} */ (event.data); | 
|  | this._addUpdateRecord(node).charDataModified(); | 
|  | // Text could be large and force us to render itself as the child in the tree outline. | 
|  | if (node.parentNode && node.parentNode.firstChild === node.parentNode.lastChild) | 
|  | this._addUpdateRecord(node.parentNode).childrenModified(); | 
|  | this._updateModifiedNodesSoon(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _nodeInserted(event) { | 
|  | const node = /** @type {!SDK.DOMNode} */ (event.data); | 
|  | this._addUpdateRecord(/** @type {!SDK.DOMNode} */ (node.parentNode)).nodeInserted(node); | 
|  | this._updateModifiedNodesSoon(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _nodeRemoved(event) { | 
|  | const node = /** @type {!SDK.DOMNode} */ (event.data.node); | 
|  | const parentNode = /** @type {!SDK.DOMNode} */ (event.data.parent); | 
|  | this.resetClipboardIfNeeded(node); | 
|  | this._addUpdateRecord(parentNode).nodeRemoved(node); | 
|  | this._updateModifiedNodesSoon(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _childNodeCountUpdated(event) { | 
|  | const node = /** @type {!SDK.DOMNode} */ (event.data); | 
|  | this._addUpdateRecord(node).childrenModified(); | 
|  | this._updateModifiedNodesSoon(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _distributedNodesChanged(event) { | 
|  | const node = /** @type {!SDK.DOMNode} */ (event.data); | 
|  | this._addUpdateRecord(node).childrenModified(); | 
|  | this._updateModifiedNodesSoon(); | 
|  | } | 
|  |  | 
|  | _updateModifiedNodesSoon() { | 
|  | if (!this._updateRecords.size) | 
|  | return; | 
|  | if (this._updateModifiedNodesTimeout) | 
|  | return; | 
|  | this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 50); | 
|  | } | 
|  |  | 
|  | _updateModifiedNodes() { | 
|  | if (this._updateModifiedNodesTimeout) { | 
|  | clearTimeout(this._updateModifiedNodesTimeout); | 
|  | delete this._updateModifiedNodesTimeout; | 
|  | } | 
|  |  | 
|  | const updatedNodes = this._updateRecords.keysArray(); | 
|  | const hidePanelWhileUpdating = updatedNodes.length > 10; | 
|  | let treeOutlineContainerElement; | 
|  | let originalScrollTop; | 
|  | if (hidePanelWhileUpdating) { | 
|  | treeOutlineContainerElement = this.element.parentNode; | 
|  | originalScrollTop = treeOutlineContainerElement ? treeOutlineContainerElement.scrollTop : 0; | 
|  | this._element.classList.add('hidden'); | 
|  | } | 
|  |  | 
|  | if (this._rootDOMNode && this._updateRecords.get(this._rootDOMNode) && | 
|  | this._updateRecords.get(this._rootDOMNode).hasChangedChildren()) { | 
|  | // Document's children have changed, perform total update. | 
|  | this.update(); | 
|  | } else { | 
|  | for (const node of this._updateRecords.keys()) { | 
|  | if (this._updateRecords.get(node).hasChangedChildren()) | 
|  | this._updateModifiedParentNode(node); | 
|  | else | 
|  | this._updateModifiedNode(node); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (hidePanelWhileUpdating) { | 
|  | this._element.classList.remove('hidden'); | 
|  | if (originalScrollTop) | 
|  | treeOutlineContainerElement.scrollTop = originalScrollTop; | 
|  | } | 
|  |  | 
|  | this._updateRecords.clear(); | 
|  | this._fireElementsTreeUpdated(updatedNodes); | 
|  | } | 
|  |  | 
|  | _updateModifiedNode(node) { | 
|  | const treeElement = this.findTreeElement(node); | 
|  | if (treeElement) | 
|  | treeElement.updateTitle(this._updateRecordForHighlight(node)); | 
|  | } | 
|  |  | 
|  | _updateModifiedParentNode(node) { | 
|  | const parentTreeElement = this.findTreeElement(node); | 
|  | if (parentTreeElement) { | 
|  | parentTreeElement.setExpandable(this._hasVisibleChildren(node)); | 
|  | parentTreeElement.updateTitle(this._updateRecordForHighlight(node)); | 
|  | if (parentTreeElement.populated) | 
|  | this._updateChildren(parentTreeElement); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | */ | 
|  | populateTreeElement(treeElement) { | 
|  | if (treeElement.childCount() || !treeElement.isExpandable()) | 
|  | return; | 
|  | treeElement.node().getChildNodes(() => this._updateModifiedParentNode(treeElement.node())); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @param {boolean=} closingTag | 
|  | * @return {!Elements.ElementsTreeElement} | 
|  | */ | 
|  | _createElementTreeElement(node, closingTag) { | 
|  | const treeElement = new Elements.ElementsTreeElement(node, closingTag); | 
|  | treeElement.setExpandable(!closingTag && this._hasVisibleChildren(node)); | 
|  | if (node.nodeType() === Node.ELEMENT_NODE && node.parentNode && node.parentNode.nodeType() === Node.DOCUMENT_NODE && | 
|  | !node.parentNode.parentNode) | 
|  | treeElement.setCollapsible(false); | 
|  | treeElement.selectable = this._selectEnabled; | 
|  | return treeElement; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | * @param {!SDK.DOMNode} child | 
|  | * @return {?Elements.ElementsTreeElement} | 
|  | */ | 
|  | _showChild(treeElement, child) { | 
|  | if (treeElement.isClosingTag()) | 
|  | return null; | 
|  |  | 
|  | const index = this._visibleChildren(treeElement.node()).indexOf(child); | 
|  | if (index === -1) | 
|  | return null; | 
|  |  | 
|  | if (index >= treeElement.expandedChildrenLimit()) | 
|  | this.setExpandedChildrenLimit(treeElement, index + 1); | 
|  | return /** @type {!Elements.ElementsTreeElement} */ (treeElement.childAt(index)); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @return {!Array.<!SDK.DOMNode>} visibleChildren | 
|  | */ | 
|  | _visibleChildren(node) { | 
|  | let visibleChildren = Elements.ElementsTreeElement.visibleShadowRoots(node); | 
|  |  | 
|  | const contentDocument = node.contentDocument(); | 
|  | if (contentDocument) | 
|  | visibleChildren.push(contentDocument); | 
|  |  | 
|  | const importedDocument = node.importedDocument(); | 
|  | if (importedDocument) | 
|  | visibleChildren.push(importedDocument); | 
|  |  | 
|  | const templateContent = node.templateContent(); | 
|  | if (templateContent) | 
|  | visibleChildren.push(templateContent); | 
|  |  | 
|  | const beforePseudoElement = node.beforePseudoElement(); | 
|  | if (beforePseudoElement) | 
|  | visibleChildren.push(beforePseudoElement); | 
|  |  | 
|  | if (node.childNodeCount()) { | 
|  | let children = node.children(); | 
|  | if (!this._showHTMLCommentsSetting.get()) | 
|  | children = children.filter(n => n.nodeType() !== Node.COMMENT_NODE); | 
|  | visibleChildren = visibleChildren.concat(children); | 
|  | } | 
|  |  | 
|  | const afterPseudoElement = node.afterPseudoElement(); | 
|  | if (afterPseudoElement) | 
|  | visibleChildren.push(afterPseudoElement); | 
|  |  | 
|  | return visibleChildren; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | * @return {boolean} | 
|  | */ | 
|  | _hasVisibleChildren(node) { | 
|  | if (node.isIframe() && Runtime.experiments.isEnabled('oopifInlineDOM')) | 
|  | return true; | 
|  | if (node.contentDocument()) | 
|  | return true; | 
|  | if (node.importedDocument()) | 
|  | return true; | 
|  | if (node.templateContent()) | 
|  | return true; | 
|  | if (Elements.ElementsTreeElement.visibleShadowRoots(node).length) | 
|  | return true; | 
|  | if (node.hasPseudoElements()) | 
|  | return true; | 
|  | if (node.isInsertionPoint()) | 
|  | return true; | 
|  | return !!node.childNodeCount() && !Elements.ElementsTreeElement.canShowInlineText(node); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | */ | 
|  | _createExpandAllButtonTreeElement(treeElement) { | 
|  | const button = UI.createTextButton('', handleLoadAllChildren.bind(this)); | 
|  | button.value = ''; | 
|  | const expandAllButtonElement = new UI.TreeElement(button); | 
|  | expandAllButtonElement.selectable = false; | 
|  | expandAllButtonElement.expandAllButton = true; | 
|  | expandAllButtonElement.button = button; | 
|  | return expandAllButtonElement; | 
|  |  | 
|  | /** | 
|  | * @this {Elements.ElementsTreeOutline} | 
|  | * @param {!Event} event | 
|  | */ | 
|  | function handleLoadAllChildren(event) { | 
|  | const visibleChildCount = this._visibleChildren(treeElement.node()).length; | 
|  | this.setExpandedChildrenLimit( | 
|  | treeElement, Math.max( | 
|  | visibleChildCount, | 
|  | treeElement.expandedChildrenLimit() + Elements.ElementsTreeElement.InitialChildrenLimit)); | 
|  | event.consume(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | * @param {number} expandedChildrenLimit | 
|  | */ | 
|  | setExpandedChildrenLimit(treeElement, expandedChildrenLimit) { | 
|  | if (treeElement.expandedChildrenLimit() === expandedChildrenLimit) | 
|  | return; | 
|  |  | 
|  | treeElement.setExpandedChildrenLimit(expandedChildrenLimit); | 
|  | if (treeElement.treeOutline && !this._treeElementsBeingUpdated.has(treeElement)) | 
|  | this._updateModifiedParentNode(treeElement.node()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | */ | 
|  | _updateChildren(treeElement) { | 
|  | if (!treeElement.isExpandable()) { | 
|  | const selectedTreeElement = treeElement.treeOutline.selectedTreeElement; | 
|  | if (selectedTreeElement && selectedTreeElement.hasAncestor(treeElement)) | 
|  | treeElement.select(true); | 
|  | treeElement.removeChildren(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | console.assert(!treeElement.isClosingTag()); | 
|  |  | 
|  | this._innerUpdateChildren(treeElement); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | * @param {!SDK.DOMNode} child | 
|  | * @param {number} index | 
|  | * @param {boolean=} closingTag | 
|  | * @return {!Elements.ElementsTreeElement} | 
|  | */ | 
|  | insertChildElement(treeElement, child, index, closingTag) { | 
|  | const newElement = this._createElementTreeElement(child, closingTag); | 
|  | treeElement.insertChild(newElement, index); | 
|  | return newElement; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | * @param {!Elements.ElementsTreeElement} child | 
|  | * @param {number} targetIndex | 
|  | */ | 
|  | _moveChild(treeElement, child, targetIndex) { | 
|  | if (treeElement.indexOfChild(child) === targetIndex) | 
|  | return; | 
|  | const wasSelected = child.selected; | 
|  | if (child.parent) | 
|  | child.parent.removeChild(child); | 
|  | treeElement.insertChild(child, targetIndex); | 
|  | if (wasSelected) | 
|  | child.select(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Elements.ElementsTreeElement} treeElement | 
|  | */ | 
|  | _innerUpdateChildren(treeElement) { | 
|  | if (this._treeElementsBeingUpdated.has(treeElement)) | 
|  | return; | 
|  |  | 
|  | this._treeElementsBeingUpdated.add(treeElement); | 
|  |  | 
|  | const node = treeElement.node(); | 
|  | const visibleChildren = this._visibleChildren(node); | 
|  | const visibleChildrenSet = new Set(visibleChildren); | 
|  |  | 
|  | // Remove any tree elements that no longer have this node as their parent and save | 
|  | // all existing elements that could be reused. This also removes closing tag element. | 
|  | const existingTreeElements = new Map(); | 
|  | for (let i = treeElement.childCount() - 1; i >= 0; --i) { | 
|  | const existingTreeElement = treeElement.childAt(i); | 
|  | if (!(existingTreeElement instanceof Elements.ElementsTreeElement)) { | 
|  | // Remove expand all button and shadow host toolbar. | 
|  | treeElement.removeChildAtIndex(i); | 
|  | continue; | 
|  | } | 
|  | const elementsTreeElement = /** @type {!Elements.ElementsTreeElement} */ (existingTreeElement); | 
|  | const existingNode = elementsTreeElement.node(); | 
|  |  | 
|  | if (visibleChildrenSet.has(existingNode)) { | 
|  | existingTreeElements.set(existingNode, existingTreeElement); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | treeElement.removeChildAtIndex(i); | 
|  | } | 
|  |  | 
|  | for (let i = 0; i < visibleChildren.length && i < treeElement.expandedChildrenLimit(); ++i) { | 
|  | const child = visibleChildren[i]; | 
|  | const existingTreeElement = existingTreeElements.get(child) || this.findTreeElement(child); | 
|  | if (existingTreeElement && existingTreeElement !== treeElement) { | 
|  | // If an existing element was found, just move it. | 
|  | this._moveChild(treeElement, existingTreeElement, i); | 
|  | } else { | 
|  | // No existing element found, insert a new element. | 
|  | const newElement = this.insertChildElement(treeElement, child, i); | 
|  | if (this._updateRecordForHighlight(node) && treeElement.expanded) | 
|  | Elements.ElementsTreeElement.animateOnDOMUpdate(newElement); | 
|  | // If a node was inserted in the middle of existing list dynamically we might need to increase the limit. | 
|  | if (treeElement.childCount() > treeElement.expandedChildrenLimit()) | 
|  | this.setExpandedChildrenLimit(treeElement, treeElement.expandedChildrenLimit() + 1); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Update expand all button. | 
|  | const expandedChildCount = treeElement.childCount(); | 
|  | if (visibleChildren.length > expandedChildCount) { | 
|  | const targetButtonIndex = expandedChildCount; | 
|  | if (!treeElement.expandAllButtonElement) | 
|  | treeElement.expandAllButtonElement = this._createExpandAllButtonTreeElement(treeElement); | 
|  | treeElement.insertChild(treeElement.expandAllButtonElement, targetButtonIndex); | 
|  | treeElement.expandAllButtonElement.button.textContent = | 
|  | Common.UIString('Show All Nodes (%d More)', visibleChildren.length - expandedChildCount); | 
|  | } else if (treeElement.expandAllButtonElement) { | 
|  | delete treeElement.expandAllButtonElement; | 
|  | } | 
|  |  | 
|  | // Insert shortcuts to distrubuted children. | 
|  | if (node.isInsertionPoint()) { | 
|  | for (const distributedNode of node.distributedNodes()) | 
|  | treeElement.appendChild(new Elements.ElementsTreeOutline.ShortcutTreeElement(distributedNode)); | 
|  | } | 
|  |  | 
|  | // Insert close tag. | 
|  | if (node.nodeType() === Node.ELEMENT_NODE && treeElement.isExpandable()) | 
|  | this.insertChildElement(treeElement, node, treeElement.childCount(), true); | 
|  |  | 
|  | this._treeElementsBeingUpdated.delete(treeElement); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _markersChanged(event) { | 
|  | const node = /** @type {!SDK.DOMNode} */ (event.data); | 
|  | const treeElement = node[this._treeElementSymbol]; | 
|  | if (treeElement) | 
|  | treeElement.updateDecorations(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | Elements.ElementsTreeOutline._treeOutlineSymbol = Symbol('treeOutline'); | 
|  |  | 
|  |  | 
|  | /** @typedef {{node: !SDK.DOMNode, isCut: boolean}} */ | 
|  | Elements.ElementsTreeOutline.ClipboardData; | 
|  |  | 
|  | /** @enum {symbol} */ | 
|  | Elements.ElementsTreeOutline.Events = { | 
|  | SelectedNodeChanged: Symbol('SelectedNodeChanged'), | 
|  | ElementsTreeUpdated: Symbol('ElementsTreeUpdated') | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * @const | 
|  | * @type {!Object.<string, string>} | 
|  | */ | 
|  | Elements.ElementsTreeOutline.MappedCharToEntity = { | 
|  | '\u00a0': 'nbsp', | 
|  | '\u0093': '#147',  // <control> | 
|  | '\u00ad': 'shy', | 
|  | '\u2002': 'ensp', | 
|  | '\u2003': 'emsp', | 
|  | '\u2009': 'thinsp', | 
|  | '\u200a': '#8202',  // Hairspace | 
|  | '\u200b': '#8203',  // ZWSP | 
|  | '\u200c': 'zwnj', | 
|  | '\u200d': 'zwj', | 
|  | '\u200e': 'lrm', | 
|  | '\u200f': 'rlm', | 
|  | '\u202a': '#8234',  // LRE | 
|  | '\u202b': '#8235',  // RLE | 
|  | '\u202c': '#8236',  // PDF | 
|  | '\u202d': '#8237',  // LRO | 
|  | '\u202e': '#8238',  // RLO | 
|  | '\ufeff': '#65279'  // BOM | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * @unrestricted | 
|  | */ | 
|  | Elements.ElementsTreeOutline.UpdateRecord = class { | 
|  | /** | 
|  | * @param {string} attrName | 
|  | */ | 
|  | attributeModified(attrName) { | 
|  | if (this._removedAttributes && this._removedAttributes.has(attrName)) | 
|  | this._removedAttributes.delete(attrName); | 
|  | if (!this._modifiedAttributes) | 
|  | this._modifiedAttributes = /** @type {!Set.<string>} */ (new Set()); | 
|  | this._modifiedAttributes.add(attrName); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} attrName | 
|  | */ | 
|  | attributeRemoved(attrName) { | 
|  | if (this._modifiedAttributes && this._modifiedAttributes.has(attrName)) | 
|  | this._modifiedAttributes.delete(attrName); | 
|  | if (!this._removedAttributes) | 
|  | this._removedAttributes = /** @type {!Set.<string>} */ (new Set()); | 
|  | this._removedAttributes.add(attrName); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!SDK.DOMNode} node | 
|  | */ | 
|  | nodeInserted(node) { | 
|  | this._hasChangedChildren = true; | 
|  | } | 
|  |  | 
|  | nodeRemoved(node) { | 
|  | this._hasChangedChildren = true; | 
|  | this._hasRemovedChildren = true; | 
|  | } | 
|  |  | 
|  | charDataModified() { | 
|  | this._charDataModified = true; | 
|  | } | 
|  |  | 
|  | childrenModified() { | 
|  | this._hasChangedChildren = true; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} attributeName | 
|  | * @return {boolean} | 
|  | */ | 
|  | isAttributeModified(attributeName) { | 
|  | return this._modifiedAttributes && this._modifiedAttributes.has(attributeName); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {boolean} | 
|  | */ | 
|  | hasRemovedAttributes() { | 
|  | return !!this._removedAttributes && !!this._removedAttributes.size; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {boolean} | 
|  | */ | 
|  | isCharDataModified() { | 
|  | return !!this._charDataModified; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {boolean} | 
|  | */ | 
|  | hasChangedChildren() { | 
|  | return !!this._hasChangedChildren; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {boolean} | 
|  | */ | 
|  | hasRemovedChildren() { | 
|  | return !!this._hasRemovedChildren; | 
|  | } | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * @implements {Common.Renderer} | 
|  | */ | 
|  | Elements.ElementsTreeOutline.Renderer = class { | 
|  | /** | 
|  | * @override | 
|  | * @param {!Object} object | 
|  | * @return {!Promise.<?Node>} | 
|  | */ | 
|  | render(object) { | 
|  | return new Promise(renderPromise); | 
|  |  | 
|  | /** | 
|  | * @param {function(!Element)} resolve | 
|  | * @param {function(!Error)} reject | 
|  | */ | 
|  | function renderPromise(resolve, reject) { | 
|  | if (object instanceof SDK.DOMNode) | 
|  | onNodeResolved(/** @type {!SDK.DOMNode} */ (object)); | 
|  | else if (object instanceof SDK.DeferredDOMNode) | 
|  | (/** @type {!SDK.DeferredDOMNode} */ (object)).resolve(onNodeResolved); | 
|  | else | 
|  | reject(new Error('Can\'t reveal not a node.')); | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {?SDK.DOMNode} node | 
|  | */ | 
|  | function onNodeResolved(node) { | 
|  | if (!node) { | 
|  | reject(new Error('Could not resolve node.')); | 
|  | return; | 
|  | } | 
|  | const treeOutline = new Elements.ElementsTreeOutline(false, false); | 
|  | treeOutline.rootDOMNode = node; | 
|  | if (!treeOutline.firstChild().isExpandable()) | 
|  | treeOutline._element.classList.add('single-node'); | 
|  | treeOutline.setVisible(true); | 
|  | treeOutline.element.treeElementForTest = treeOutline.firstChild(); | 
|  | resolve(treeOutline.element); | 
|  | } | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * @unrestricted | 
|  | */ | 
|  | Elements.ElementsTreeOutline.ShortcutTreeElement = class extends UI.TreeElement { | 
|  | /** | 
|  | * @param {!SDK.DOMNodeShortcut} nodeShortcut | 
|  | */ | 
|  | constructor(nodeShortcut) { | 
|  | super(''); | 
|  | this.listItemElement.createChild('div', 'selection fill'); | 
|  | const title = this.listItemElement.createChild('span', 'elements-tree-shortcut-title'); | 
|  | let text = nodeShortcut.nodeName.toLowerCase(); | 
|  | if (nodeShortcut.nodeType === Node.ELEMENT_NODE) | 
|  | text = '<' + text + '>'; | 
|  | title.textContent = '\u21AA ' + text; | 
|  |  | 
|  | const link = Elements.DOMLinkifier.linkifyDeferredNodeReference(nodeShortcut.deferredNode); | 
|  | this.listItemElement.createTextChild(' '); | 
|  | link.classList.add('elements-tree-shortcut-link'); | 
|  | link.textContent = Common.UIString('reveal'); | 
|  | this.listItemElement.appendChild(link); | 
|  | this._nodeShortcut = nodeShortcut; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {boolean} | 
|  | */ | 
|  | get hovered() { | 
|  | return this._hovered; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {boolean} x | 
|  | */ | 
|  | set hovered(x) { | 
|  | if (this._hovered === x) | 
|  | return; | 
|  | this._hovered = x; | 
|  | this.listItemElement.classList.toggle('hovered', x); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {number} | 
|  | */ | 
|  | backendNodeId() { | 
|  | return this._nodeShortcut.deferredNode.backendNodeId(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {!SDK.DOMModel} | 
|  | */ | 
|  | domModel() { | 
|  | return this._nodeShortcut.deferredNode.domModel(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @override | 
|  | * @param {boolean=} selectedByUser | 
|  | * @return {boolean} | 
|  | */ | 
|  | onselect(selectedByUser) { | 
|  | if (!selectedByUser) | 
|  | return true; | 
|  | this._nodeShortcut.deferredNode.highlight(); | 
|  | this._nodeShortcut.deferredNode.resolve(resolved.bind(this)); | 
|  | /** | 
|  | * @param {?SDK.DOMNode} node | 
|  | * @this {Elements.ElementsTreeOutline.ShortcutTreeElement} | 
|  | */ | 
|  | function resolved(node) { | 
|  | if (node) { | 
|  | this.treeOutline._selectedDOMNode = node; | 
|  | this.treeOutline._selectedNodeChanged(); | 
|  | } | 
|  | } | 
|  | return true; | 
|  | } | 
|  | }; |