| /* |
| * 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. |
| */ |
| |
| /** |
| * @constructor |
| * @extends {TreeOutline} |
| * @param {!WebInspector.DOMModel} domModel |
| * @param {boolean=} omitRootDOMNode |
| * @param {boolean=} selectEnabled |
| */ |
| WebInspector.ElementsTreeOutline = function(domModel, omitRootDOMNode, selectEnabled) |
| { |
| this._domModel = domModel; |
| this._treeElementSymbol = Symbol("treeElement"); |
| |
| var element = createElement("div"); |
| |
| this._shadowRoot = WebInspector.createShadowRootWithCoreStyles(element, "elements/elementsTreeOutline.css"); |
| var outlineDisclosureElement = this._shadowRoot.createChild("div", "elements-disclosure"); |
| |
| TreeOutline.call(this); |
| this._element = this.element; |
| this._element.classList.add("elements-tree-outline", "source-code"); |
| 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); |
| |
| outlineDisclosureElement.appendChild(this._element); |
| this.element = element; |
| |
| this._includeRootDOMNode = !omitRootDOMNode; |
| this._selectEnabled = selectEnabled; |
| /** @type {?WebInspector.DOMNode} */ |
| this._rootDOMNode = null; |
| /** @type {?WebInspector.DOMNode} */ |
| this._selectedDOMNode = null; |
| |
| this._visible = false; |
| |
| this._popoverHelper = new WebInspector.PopoverHelper(this._element, this._getPopoverAnchor.bind(this), this._showPopover.bind(this)); |
| this._popoverHelper.setTimeout(0, 100); |
| |
| /** @type {!Map<!WebInspector.DOMNode, !WebInspector.ElementsTreeOutline.UpdateRecord>} */ |
| this._updateRecords = new Map(); |
| /** @type {!Set<!WebInspector.ElementsTreeElement>} */ |
| this._treeElementsBeingUpdated = new Set(); |
| |
| this._domModel.addEventListener(WebInspector.DOMModel.Events.MarkersChanged, this._markersChanged, this); |
| } |
| |
| /** @typedef {{node: !WebInspector.DOMNode, isCut: boolean}} */ |
| WebInspector.ElementsTreeOutline.ClipboardData; |
| |
| /** |
| * @enum {string} |
| */ |
| WebInspector.ElementsTreeOutline.Events = { |
| SelectedNodeChanged: "SelectedNodeChanged", |
| ElementsTreeUpdated: "ElementsTreeUpdated" |
| } |
| |
| /** |
| * @const |
| * @type {!Object.<string, string>} |
| */ |
| WebInspector.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 |
| } |
| |
| WebInspector.ElementsTreeOutline.prototype = { |
| /** |
| * @return {symbol} |
| */ |
| treeElementSymbol: function() |
| { |
| return this._treeElementSymbol; |
| }, |
| |
| focus: function() |
| { |
| this._element.focus(); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| hasFocus: function() |
| { |
| return this._element === WebInspector.currentFocusElement(); |
| }, |
| |
| /** |
| * @param {boolean} wrap |
| */ |
| setWordWrap: function(wrap) |
| { |
| this._element.classList.toggle("elements-tree-nowrap", !wrap); |
| }, |
| |
| /** |
| * @return {!WebInspector.DOMModel} |
| */ |
| domModel: function() |
| { |
| return this._domModel; |
| }, |
| |
| /** |
| * @param {?WebInspector.InplaceEditor.Controller} multilineEditing |
| */ |
| setMultilineEditing: function(multilineEditing) |
| { |
| this._multilineEditing = multilineEditing; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| visibleWidth: function() |
| { |
| return this._visibleWidth; |
| }, |
| |
| /** |
| * @param {number} width |
| */ |
| setVisibleWidth: function(width) |
| { |
| this._visibleWidth = width; |
| if (this._multilineEditing) |
| this._multilineEditing.setWidth(this._visibleWidth); |
| }, |
| |
| /** |
| * @param {?WebInspector.ElementsTreeOutline.ClipboardData} data |
| */ |
| _setClipboardData: function(data) |
| { |
| if (this._clipboardNodeData) { |
| var treeElement = this.findTreeElement(this._clipboardNodeData.node); |
| if (treeElement) |
| treeElement.setInClipboard(false); |
| delete this._clipboardNodeData; |
| } |
| |
| if (data) { |
| var treeElement = this.findTreeElement(data.node); |
| if (treeElement) |
| treeElement.setInClipboard(true); |
| this._clipboardNodeData = data; |
| } |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} removedNode |
| */ |
| resetClipboardIfNeeded: function(removedNode) |
| { |
| if (this._clipboardNodeData && this._clipboardNodeData.node === removedNode) |
| this._setClipboardData(null); |
| }, |
| |
| /** |
| * @param {boolean} isCut |
| * @param {!Event} event |
| */ |
| handleCopyOrCutKeyboardEvent: function(isCut, event) |
| { |
| this._setClipboardData(null); |
| |
| // Don't prevent the normal copy if the user has a selection. |
| if (!event.target.isComponentSelectionCollapsed()) |
| return; |
| |
| // Do not interfere with text editing. |
| if (WebInspector.isEditing()) |
| return; |
| |
| var targetNode = this.selectedDOMNode(); |
| if (!targetNode) |
| return; |
| |
| event.clipboardData.clearData(); |
| event.preventDefault(); |
| |
| this.performCopyOrCut(isCut, targetNode); |
| }, |
| |
| /** |
| * @param {boolean} isCut |
| * @param {?WebInspector.DOMNode} node |
| */ |
| performCopyOrCut: function(isCut, node) |
| { |
| if (isCut && (node.isShadowRoot() || node.ancestorUserAgentShadowRoot())) |
| return; |
| |
| node.copyNode(); |
| this._setClipboardData({ node: node, isCut: isCut }); |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} targetNode |
| * @return {boolean} |
| */ |
| canPaste: function(targetNode) |
| { |
| if (targetNode.isShadowRoot() || targetNode.ancestorUserAgentShadowRoot()) |
| return false; |
| |
| if (!this._clipboardNodeData) |
| return false; |
| |
| var node = this._clipboardNodeData.node; |
| if (this._clipboardNodeData.isCut && (node === targetNode || node.isAncestor(targetNode))) |
| return false; |
| |
| if (targetNode.target() !== node.target()) |
| return false; |
| return true; |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} targetNode |
| */ |
| pasteNode: function(targetNode) |
| { |
| if (this.canPaste(targetNode)) |
| this._performPaste(targetNode); |
| }, |
| |
| /** |
| * @param {!Event} event |
| */ |
| handlePasteKeyboardEvent: function(event) |
| { |
| // Do not interfere with text editing. |
| if (WebInspector.isEditing()) |
| return; |
| |
| var targetNode = this.selectedDOMNode(); |
| if (!targetNode || !this.canPaste(targetNode)) |
| return; |
| |
| event.preventDefault(); |
| this._performPaste(targetNode); |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} targetNode |
| */ |
| _performPaste: function(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 {!DOMAgent.NodeId} nodeId |
| * @this {WebInspector.ElementsTreeOutline} |
| */ |
| function expandCallback(error, nodeId) |
| { |
| if (error) |
| return; |
| var pastedNode = this._domModel.nodeForId(nodeId); |
| if (!pastedNode) |
| return; |
| this.selectDOMNode(pastedNode); |
| } |
| }, |
| |
| /** |
| * @param {boolean} visible |
| */ |
| setVisible: function(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 {?WebInspector.DOMNode} |
| */ |
| selectedDOMNode: function() |
| { |
| return this._selectedDOMNode; |
| }, |
| |
| /** |
| * @param {?WebInspector.DOMNode} node |
| * @param {boolean=} focus |
| */ |
| selectDOMNode: function(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(); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| editing: function() |
| { |
| var node = this.selectedDOMNode(); |
| if (!node) |
| return false; |
| var treeElement = this.findTreeElement(node); |
| if (!treeElement) |
| return false; |
| return treeElement.isEditing() || false; |
| }, |
| |
| update: function() |
| { |
| var selectedTreeElement = this.selectedTreeElement; |
| if (!(selectedTreeElement instanceof WebInspector.ElementsTreeElement)) |
| selectedTreeElement = null; |
| |
| var selectedNode = selectedTreeElement ? selectedTreeElement.node() : null; |
| |
| this.removeChildren(); |
| |
| if (!this.rootDOMNode) |
| return; |
| |
| var treeElement; |
| if (this._includeRootDOMNode) { |
| treeElement = this._createElementTreeElement(this.rootDOMNode); |
| this.appendChild(treeElement); |
| } else { |
| // FIXME: this could use findTreeElement to reuse a tree element if it already exists |
| var node = this.rootDOMNode.firstChild; |
| while (node) { |
| treeElement = this._createElementTreeElement(node); |
| this.appendChild(treeElement); |
| node = node.nextSibling; |
| } |
| } |
| |
| if (selectedNode) |
| this._revealAndSelectNode(selectedNode, true); |
| }, |
| |
| updateSelection: function() |
| { |
| if (!this.selectedTreeElement) |
| return; |
| var element = this.selectedTreeElement; |
| element.updateSelection(); |
| }, |
| |
| _selectedNodeChanged: function() |
| { |
| this.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedDOMNode); |
| }, |
| |
| /** |
| * @param {!Array.<!WebInspector.DOMNode>} nodes |
| */ |
| _fireElementsTreeUpdated: function(nodes) |
| { |
| this.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.ElementsTreeUpdated, nodes); |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} node |
| * @return {?WebInspector.ElementsTreeElement} |
| */ |
| findTreeElement: function(node) |
| { |
| var 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 {?WebInspector.ElementsTreeElement} */ (treeElement); |
| }, |
| |
| /** |
| * @param {?WebInspector.DOMNode} node |
| * @return {?TreeElement} |
| */ |
| _lookUpTreeElement: function(node) |
| { |
| if (!node) |
| return null; |
| |
| var cachedElement = node[this._treeElementSymbol]; |
| if (cachedElement) |
| return cachedElement; |
| |
| // Walk up the parent pointers from the desired node |
| var ancestors = []; |
| for (var 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 (var i = ancestors.length - 1; i >= 0; --i) { |
| var treeElement = ancestors[i][this._treeElementSymbol]; |
| if (treeElement) |
| treeElement.onpopulate(); // fill the cache with the children of treeElement |
| } |
| |
| return node[this._treeElementSymbol]; |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} node |
| * @return {?WebInspector.ElementsTreeElement} |
| */ |
| createTreeElementFor: function(node) |
| { |
| var 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 {?WebInspector.DOMNode} node |
| * @param {boolean} omitFocus |
| */ |
| _revealAndSelectNode: function(node, omitFocus) |
| { |
| if (this._suppressRevealAndSelect) |
| return; |
| |
| if (!this._includeRootDOMNode && node === this.rootDOMNode && this.rootDOMNode) |
| node = this.rootDOMNode.firstChild; |
| if (!node) |
| return; |
| var treeElement = this.createTreeElementFor(node); |
| if (!treeElement) |
| return; |
| |
| treeElement.revealAndSelect(omitFocus); |
| }, |
| |
| /** |
| * @return {?TreeElement} |
| */ |
| _treeElementFromEvent: function(event) |
| { |
| var 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. |
| var x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36; |
| |
| var 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. |
| var elementUnderMouse = this.treeElementFromPoint(x, y); |
| var elementAboveMouse = this.treeElementFromPoint(x, y - 2); |
| var element; |
| if (elementUnderMouse === elementAboveMouse) |
| element = elementUnderMouse; |
| else |
| element = this.treeElementFromPoint(x, y + 2); |
| |
| return element; |
| }, |
| |
| /** |
| * @param {!Element} element |
| * @param {!Event} event |
| * @return {!Element|!AnchorBox|undefined} |
| */ |
| _getPopoverAnchor: function(element, event) |
| { |
| var anchor = element.enclosingNodeOrSelfWithClass("webkit-html-resource-link"); |
| if (!anchor || !anchor.href) |
| return; |
| |
| return anchor; |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} node |
| * @param {function()} callback |
| */ |
| _loadDimensionsForNode: function(node, callback) |
| { |
| if (!node.nodeName() || node.nodeName().toLowerCase() !== "img") { |
| callback(); |
| return; |
| } |
| |
| node.resolveToObject("", resolvedNode); |
| |
| function resolvedNode(object) |
| { |
| if (!object) { |
| callback(); |
| return; |
| } |
| |
| object.callFunctionJSON(features, undefined, callback); |
| object.release(); |
| |
| /** |
| * @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 }; |
| } |
| } |
| }, |
| |
| /** |
| * @param {!Element} anchor |
| * @param {!WebInspector.Popover} popover |
| */ |
| _showPopover: function(anchor, popover) |
| { |
| var listItem = anchor.enclosingNodeOrSelfWithNodeName("li"); |
| var node = /** @type {!WebInspector.ElementsTreeElement} */ (listItem.treeElement).node(); |
| this._loadDimensionsForNode(node, WebInspector.DOMPresentationUtils.buildImagePreviewContents.bind(WebInspector.DOMPresentationUtils, node.target(), anchor.href, true, showPopover)); |
| |
| /** |
| * @param {!Element=} contents |
| */ |
| function showPopover(contents) |
| { |
| if (!contents) |
| return; |
| popover.setCanShrink(false); |
| popover.showForAnchor(contents, anchor); |
| } |
| }, |
| |
| _onmousedown: function(event) |
| { |
| var element = this._treeElementFromEvent(event); |
| |
| if (!element || element.isEventWithinDisclosureTriangle(event)) |
| return; |
| |
| element.select(); |
| }, |
| |
| /** |
| * @param {?TreeElement} treeElement |
| */ |
| setHoverEffect: function(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: function(event) |
| { |
| var element = this._treeElementFromEvent(event); |
| if (element && this._previousHoveredElement === element) |
| return; |
| |
| this.setHoverEffect(element); |
| |
| if (element instanceof WebInspector.ElementsTreeElement) { |
| this._domModel.highlightDOMNodeWithConfig(element.node().id, { mode: "all", showInfo: !WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) }); |
| return; |
| } |
| |
| if (element instanceof WebInspector.ElementsTreeOutline.ShortcutTreeElement) |
| this._domModel.highlightDOMNodeWithConfig(undefined, { mode: "all", showInfo: !WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) }, element.backendNodeId()); |
| }, |
| |
| _onmouseleave: function(event) |
| { |
| this.setHoverEffect(null); |
| WebInspector.DOMModel.hideDOMNodeHighlight(); |
| }, |
| |
| _ondragstart: function(event) |
| { |
| if (!event.target.isComponentSelectionCollapsed()) |
| return false; |
| if (event.target.nodeName === "A") |
| return false; |
| |
| var treeElement = this._treeElementFromEvent(event); |
| if (!this._isValidDragSourceOrTarget(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; |
| |
| WebInspector.DOMModel.hideDOMNodeHighlight(); |
| |
| return true; |
| }, |
| |
| _ondragover: function(event) |
| { |
| if (!this._treeElementBeingDragged) |
| return false; |
| |
| var treeElement = this._treeElementFromEvent(event); |
| if (!this._isValidDragSourceOrTarget(treeElement)) |
| return false; |
| |
| var node = treeElement.node(); |
| while (node) { |
| if (node === this._treeElementBeingDragged._node) |
| return false; |
| node = node.parentNode; |
| } |
| |
| treeElement.updateSelection(); |
| treeElement.listItemElement.classList.add("elements-drag-over"); |
| this._dragOverTreeElement = treeElement; |
| event.preventDefault(); |
| event.dataTransfer.dropEffect = "move"; |
| return false; |
| }, |
| |
| _ondragleave: function(event) |
| { |
| this._clearDragOverTreeElementMarker(); |
| event.preventDefault(); |
| return false; |
| }, |
| |
| /** |
| * @param {?TreeElement} treeElement |
| * @return {boolean} |
| */ |
| _isValidDragSourceOrTarget: function(treeElement) |
| { |
| if (!treeElement) |
| return false; |
| |
| if (!(treeElement instanceof WebInspector.ElementsTreeElement)) |
| return false; |
| var elementsTreeElement = /** @type {!WebInspector.ElementsTreeElement} */ (treeElement); |
| |
| var node = elementsTreeElement.node(); |
| if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE) |
| return false; |
| |
| return true; |
| }, |
| |
| _ondrop: function(event) |
| { |
| event.preventDefault(); |
| var treeElement = this._treeElementFromEvent(event); |
| if (treeElement) |
| this._doMove(treeElement); |
| }, |
| |
| /** |
| * @param {!TreeElement} treeElement |
| */ |
| _doMove: function(treeElement) |
| { |
| if (!this._treeElementBeingDragged) |
| return; |
| |
| var parentNode; |
| var anchorNode; |
| |
| if (treeElement.isClosingTag()) { |
| // Drop onto closing tag -> insert as last child. |
| parentNode = treeElement.node(); |
| } else { |
| var dragTargetNode = treeElement.node(); |
| parentNode = dragTargetNode.parentNode; |
| anchorNode = dragTargetNode; |
| } |
| |
| var wasExpanded = this._treeElementBeingDragged.expanded; |
| this._treeElementBeingDragged._node.moveTo(parentNode, anchorNode, this.selectNodeAfterEdit.bind(this, wasExpanded)); |
| |
| delete this._treeElementBeingDragged; |
| }, |
| |
| _ondragend: function(event) |
| { |
| event.preventDefault(); |
| this._clearDragOverTreeElementMarker(); |
| delete this._treeElementBeingDragged; |
| }, |
| |
| _clearDragOverTreeElementMarker: function() |
| { |
| if (this._dragOverTreeElement) { |
| this._dragOverTreeElement.updateSelection(); |
| this._dragOverTreeElement.listItemElement.classList.remove("elements-drag-over"); |
| delete this._dragOverTreeElement; |
| } |
| }, |
| |
| _contextMenuEventFired: function(event) |
| { |
| var treeElement = this._treeElementFromEvent(event); |
| if (treeElement instanceof WebInspector.ElementsTreeElement) |
| this.showContextMenu(treeElement, event); |
| }, |
| |
| /** |
| * @param {!WebInspector.ElementsTreeElement} treeElement |
| * @param {!Event} event |
| */ |
| showContextMenu: function(treeElement, event) |
| { |
| if (WebInspector.isEditing()) |
| return; |
| |
| var contextMenu = new WebInspector.ContextMenu(event); |
| var isPseudoElement = !!treeElement.node().pseudoType(); |
| var isTag = treeElement.node().nodeType() === Node.ELEMENT_NODE && !isPseudoElement; |
| var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node"); |
| if (textNode && textNode.classList.contains("bogus")) |
| textNode = null; |
| var commentNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-comment"); |
| contextMenu.appendApplicableItems(event.target); |
| if (textNode) { |
| contextMenu.appendSeparator(); |
| treeElement.populateTextContextMenu(contextMenu, textNode); |
| } else if (isTag) { |
| contextMenu.appendSeparator(); |
| treeElement.populateTagContextMenu(contextMenu, event); |
| } else if (commentNode) { |
| contextMenu.appendSeparator(); |
| treeElement.populateNodeContextMenu(contextMenu); |
| } else if (isPseudoElement) { |
| treeElement.populateScrollIntoView(contextMenu); |
| } |
| |
| contextMenu.appendApplicableItems(treeElement.node()); |
| contextMenu.show(); |
| }, |
| |
| runPendingUpdates: function() |
| { |
| this._updateModifiedNodes(); |
| }, |
| |
| handleShortcut: function(event) |
| { |
| var node = this.selectedDOMNode(); |
| if (!node) |
| return; |
| var treeElement = node[this._treeElementSymbol]; |
| if (!treeElement) |
| return; |
| |
| if (WebInspector.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 {!WebInspector.DOMNode} node |
| * @param {boolean=} startEditing |
| * @param {function()=} callback |
| */ |
| toggleEditAsHTML: function(node, startEditing, callback) |
| { |
| var treeElement = node[this._treeElementSymbol]; |
| if (!treeElement || !treeElement.hasEditableNode()) |
| return; |
| |
| if (node.pseudoType()) |
| return; |
| |
| var parentNode = node.parentNode; |
| var index = node.index; |
| var wasExpanded = treeElement.expanded; |
| |
| treeElement.toggleEditAsHTML(editingFinished.bind(this), startEditing); |
| |
| /** |
| * @this {WebInspector.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(); |
| |
| var newNode = parentNode ? parentNode.children()[index] || parentNode : null; |
| if (!newNode) |
| return; |
| |
| this.selectDOMNode(newNode, true); |
| |
| if (wasExpanded) { |
| var newTreeItem = this.findTreeElement(newNode); |
| if (newTreeItem) |
| newTreeItem.expand(); |
| } |
| } |
| }, |
| |
| /** |
| * @param {boolean} wasExpanded |
| * @param {?Protocol.Error} error |
| * @param {!DOMAgent.NodeId=} nodeId |
| * @return {?WebInspector.ElementsTreeElement} nodeId |
| */ |
| selectNodeAfterEdit: function(wasExpanded, error, nodeId) |
| { |
| 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(); |
| |
| var newNode = nodeId ? this._domModel.nodeForId(nodeId) : null; |
| if (!newNode) |
| return null; |
| |
| this.selectDOMNode(newNode, true); |
| |
| var 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 {!WebInspector.DOMNode} node |
| * @param {function(?WebInspector.RemoteObject, boolean=)=} userCallback |
| */ |
| toggleHideElement: function(node, userCallback) |
| { |
| var pseudoType = node.pseudoType(); |
| var effectiveNode = pseudoType ? node.parentNode : node; |
| if (!effectiveNode) |
| return; |
| |
| var hidden = node.marker("hidden-marker"); |
| |
| function resolvedNode(object) |
| { |
| if (!object) |
| return; |
| |
| /** |
| * @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__"; |
| var 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"); |
| var selector = selectors.join(", "); |
| var ruleBody = " visibility: hidden !important;"; |
| var rule = "\n" + selector + "\n{\n" + ruleBody + "\n}\n"; |
| var className = classNamePrefix + (pseudoType || "") + classNameSuffix; |
| this.classList.toggle(className, hidden); |
| |
| var localRoot = this; |
| while (localRoot.parentNode) |
| localRoot = localRoot.parentNode; |
| if (localRoot.nodeType === Node.DOCUMENT_NODE) |
| localRoot = document.head; |
| |
| var 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); |
| } |
| |
| object.callFunction(toggleClassAndInjectStyleRule, [{ value: pseudoType }, { value: !hidden}], userCallback); |
| object.release(); |
| node.setMarker("hidden-marker", hidden ? null : true); |
| } |
| |
| effectiveNode.resolveToObject("", resolvedNode); |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} node |
| * @return {boolean} |
| */ |
| isToggledToHidden: function(node) |
| { |
| return !!node.marker("hidden-marker"); |
| }, |
| |
| _reset: function() |
| { |
| this.rootDOMNode = null; |
| this.selectDOMNode(null, false); |
| this._popoverHelper.hidePopover(); |
| delete this._clipboardNodeData; |
| WebInspector.DOMModel.hideDOMNodeHighlight(); |
| this._updateRecords.clear(); |
| }, |
| |
| wireToDOMModel: function() |
| { |
| this._domModel.addEventListener(WebInspector.DOMModel.Events.NodeInserted, this._nodeInserted, this); |
| this._domModel.addEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this); |
| this._domModel.addEventListener(WebInspector.DOMModel.Events.AttrModified, this._attributeModified, this); |
| this._domModel.addEventListener(WebInspector.DOMModel.Events.AttrRemoved, this._attributeRemoved, this); |
| this._domModel.addEventListener(WebInspector.DOMModel.Events.CharacterDataModified, this._characterDataModified, this); |
| this._domModel.addEventListener(WebInspector.DOMModel.Events.DocumentUpdated, this._documentUpdated, this); |
| this._domModel.addEventListener(WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this); |
| this._domModel.addEventListener(WebInspector.DOMModel.Events.DistributedNodesChanged, this._distributedNodesChanged, this); |
| }, |
| |
| unwireFromDOMModel: function() |
| { |
| this._domModel.removeEventListener(WebInspector.DOMModel.Events.NodeInserted, this._nodeInserted, this); |
| this._domModel.removeEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this); |
| this._domModel.removeEventListener(WebInspector.DOMModel.Events.AttrModified, this._attributeModified, this); |
| this._domModel.removeEventListener(WebInspector.DOMModel.Events.AttrRemoved, this._attributeRemoved, this); |
| this._domModel.removeEventListener(WebInspector.DOMModel.Events.CharacterDataModified, this._characterDataModified, this); |
| this._domModel.removeEventListener(WebInspector.DOMModel.Events.DocumentUpdated, this._documentUpdated, this); |
| this._domModel.removeEventListener(WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this); |
| this._domModel.removeEventListener(WebInspector.DOMModel.Events.DistributedNodesChanged, this._distributedNodesChanged, this); |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} node |
| * @return {!WebInspector.ElementsTreeOutline.UpdateRecord} |
| */ |
| _addUpdateRecord: function(node) |
| { |
| var record = this._updateRecords.get(node); |
| if (!record) { |
| record = new WebInspector.ElementsTreeOutline.UpdateRecord(); |
| this._updateRecords.set(node, record); |
| } |
| return record; |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} node |
| * @return {?WebInspector.ElementsTreeOutline.UpdateRecord} |
| */ |
| _updateRecordForHighlight: function(node) |
| { |
| if (!this._visible) |
| return null; |
| return this._updateRecords.get(node) || null; |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _documentUpdated: function(event) |
| { |
| var inspectedRootDocument = event.data; |
| |
| this._reset(); |
| |
| if (!inspectedRootDocument) |
| return; |
| |
| this.rootDOMNode = inspectedRootDocument; |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _attributeModified: function(event) |
| { |
| var node = /** @type {!WebInspector.DOMNode} */ (event.data.node); |
| this._addUpdateRecord(node).attributeModified(event.data.name); |
| this._updateModifiedNodesSoon(); |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _attributeRemoved: function(event) |
| { |
| var node = /** @type {!WebInspector.DOMNode} */ (event.data.node); |
| this._addUpdateRecord(node).attributeRemoved(event.data.name); |
| this._updateModifiedNodesSoon(); |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _characterDataModified: function(event) |
| { |
| var node = /** @type {!WebInspector.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 {!WebInspector.Event} event |
| */ |
| _nodeInserted: function(event) |
| { |
| var node = /** @type {!WebInspector.DOMNode} */ (event.data); |
| this._addUpdateRecord(/** @type {!WebInspector.DOMNode} */ (node.parentNode)).nodeInserted(node); |
| this._updateModifiedNodesSoon(); |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _nodeRemoved: function(event) |
| { |
| var node = /** @type {!WebInspector.DOMNode} */ (event.data.node); |
| var parentNode = /** @type {!WebInspector.DOMNode} */ (event.data.parent); |
| this.resetClipboardIfNeeded(node); |
| this._addUpdateRecord(parentNode).nodeRemoved(node); |
| this._updateModifiedNodesSoon(); |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _childNodeCountUpdated: function(event) |
| { |
| var node = /** @type {!WebInspector.DOMNode} */ (event.data); |
| this._addUpdateRecord(node).childrenModified(); |
| this._updateModifiedNodesSoon(); |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _distributedNodesChanged: function(event) |
| { |
| var node = /** @type {!WebInspector.DOMNode} */ (event.data); |
| this._addUpdateRecord(node).childrenModified(); |
| this._updateModifiedNodesSoon(); |
| }, |
| |
| _updateModifiedNodesSoon: function() |
| { |
| if (!this._updateRecords.size) |
| return; |
| if (this._updateModifiedNodesTimeout) |
| return; |
| this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 50); |
| }, |
| |
| _updateModifiedNodes: function() |
| { |
| if (this._updateModifiedNodesTimeout) { |
| clearTimeout(this._updateModifiedNodesTimeout); |
| delete this._updateModifiedNodesTimeout; |
| } |
| |
| var updatedNodes = this._updateRecords.keysArray(); |
| var hidePanelWhileUpdating = updatedNodes.length > 10; |
| if (hidePanelWhileUpdating) { |
| var treeOutlineContainerElement = this.element.parentNode; |
| var 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 (var 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.updateSelection(); |
| } |
| |
| this._updateRecords.clear(); |
| this._fireElementsTreeUpdated(updatedNodes); |
| }, |
| |
| _updateModifiedNode: function(node) |
| { |
| var treeElement = this.findTreeElement(node); |
| if (treeElement) |
| treeElement.updateTitle(this._updateRecordForHighlight(node)); |
| }, |
| |
| _updateModifiedParentNode: function(node) |
| { |
| var parentTreeElement = this.findTreeElement(node); |
| if (parentTreeElement) { |
| parentTreeElement.setExpandable(this._hasVisibleChildren(node)); |
| parentTreeElement.updateTitle(this._updateRecordForHighlight(node)); |
| if (parentTreeElement.populated) |
| this._updateChildren(parentTreeElement); |
| } |
| }, |
| |
| /** |
| * @param {!WebInspector.ElementsTreeElement} treeElement |
| */ |
| populateTreeElement: function(treeElement) |
| { |
| if (treeElement.childCount() || !treeElement.isExpandable()) |
| return; |
| |
| this._updateModifiedParentNode(treeElement.node()); |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} node |
| * @param {boolean=} closingTag |
| * @return {!WebInspector.ElementsTreeElement} |
| */ |
| _createElementTreeElement: function(node, closingTag) |
| { |
| var treeElement = new WebInspector.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 {!WebInspector.ElementsTreeElement} treeElement |
| * @param {!WebInspector.DOMNode} child |
| * @return {?WebInspector.ElementsTreeElement} |
| */ |
| _showChild: function(treeElement, child) |
| { |
| if (treeElement.isClosingTag()) |
| return null; |
| |
| var index = this._visibleChildren(treeElement.node()).indexOf(child); |
| if (index === -1) |
| return null; |
| |
| if (index >= treeElement.expandedChildrenLimit()) |
| this.setExpandedChildrenLimit(treeElement, index + 1); |
| return /** @type {!WebInspector.ElementsTreeElement} */ (treeElement.childAt(index)); |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} node |
| * @return {!Array.<!WebInspector.DOMNode>} visibleChildren |
| */ |
| _visibleChildren: function(node) |
| { |
| var visibleChildren = WebInspector.ElementsTreeElement.visibleShadowRoots(node); |
| |
| var importedDocument = node.importedDocument(); |
| if (importedDocument) |
| visibleChildren.push(importedDocument); |
| |
| var templateContent = node.templateContent(); |
| if (templateContent) |
| visibleChildren.push(templateContent); |
| |
| var beforePseudoElement = node.beforePseudoElement(); |
| if (beforePseudoElement) |
| visibleChildren.push(beforePseudoElement); |
| |
| if (node.childNodeCount()) |
| visibleChildren = visibleChildren.concat(node.children()); |
| |
| var afterPseudoElement = node.afterPseudoElement(); |
| if (afterPseudoElement) |
| visibleChildren.push(afterPseudoElement); |
| |
| return visibleChildren; |
| }, |
| |
| /** |
| * @param {!WebInspector.DOMNode} node |
| * @return {boolean} |
| */ |
| _hasVisibleChildren: function(node) |
| { |
| if (node.importedDocument()) |
| return true; |
| if (node.templateContent()) |
| return true; |
| if (WebInspector.ElementsTreeElement.visibleShadowRoots(node).length) |
| return true; |
| if (node.hasPseudoElements()) |
| return true; |
| if (node.isInsertionPoint()) |
| return true; |
| return !!node.childNodeCount() && !WebInspector.ElementsTreeElement.canShowInlineText(node); |
| }, |
| |
| /** |
| * @param {!WebInspector.ElementsTreeElement} treeElement |
| */ |
| _createExpandAllButtonTreeElement: function(treeElement) |
| { |
| var button = createTextButton("", handleLoadAllChildren.bind(this)); |
| button.value = ""; |
| var expandAllButtonElement = new TreeElement(button); |
| expandAllButtonElement.selectable = false; |
| expandAllButtonElement.expandAllButton = true; |
| expandAllButtonElement.button = button; |
| return expandAllButtonElement; |
| |
| /** |
| * @this {WebInspector.ElementsTreeOutline} |
| * @param {!Event} event |
| */ |
| function handleLoadAllChildren(event) |
| { |
| var visibleChildCount = this._visibleChildren(treeElement.node()).length; |
| this.setExpandedChildrenLimit(treeElement, Math.max(visibleChildCount, treeElement.expandedChildrenLimit() + WebInspector.ElementsTreeElement.InitialChildrenLimit)); |
| event.consume(); |
| } |
| }, |
| |
| /** |
| * @param {!WebInspector.ElementsTreeElement} treeElement |
| * @param {number} expandedChildrenLimit |
| */ |
| setExpandedChildrenLimit: function(treeElement, expandedChildrenLimit) |
| { |
| if (treeElement.expandedChildrenLimit() === expandedChildrenLimit) |
| return; |
| |
| treeElement.setExpandedChildrenLimit(expandedChildrenLimit); |
| if (treeElement.treeOutline && !this._treeElementsBeingUpdated.has(treeElement)) |
| this._updateModifiedParentNode(treeElement.node()); |
| }, |
| |
| /** |
| * @param {!WebInspector.ElementsTreeElement} treeElement |
| */ |
| _updateChildren: function(treeElement) |
| { |
| if (!treeElement.isExpandable()) { |
| var selectedTreeElement = treeElement.treeOutline.selectedTreeElement; |
| if (selectedTreeElement && selectedTreeElement.hasAncestor(treeElement)) |
| treeElement.select(true); |
| treeElement.removeChildren(); |
| return; |
| } |
| |
| console.assert(!treeElement.isClosingTag()); |
| |
| treeElement.node().getChildNodes(childNodesLoaded.bind(this)); |
| |
| /** |
| * @param {?Array.<!WebInspector.DOMNode>} children |
| * @this {WebInspector.ElementsTreeOutline} |
| */ |
| function childNodesLoaded(children) |
| { |
| // FIXME: sort this out, it should not happen. |
| if (!children) |
| return; |
| this._innerUpdateChildren(treeElement); |
| } |
| }, |
| |
| /** |
| * @param {!WebInspector.ElementsTreeElement} treeElement |
| * @param {!WebInspector.DOMNode} child |
| * @param {number} index |
| * @param {boolean=} closingTag |
| * @return {!WebInspector.ElementsTreeElement} |
| */ |
| insertChildElement: function(treeElement, child, index, closingTag) |
| { |
| var newElement = this._createElementTreeElement(child, closingTag); |
| treeElement.insertChild(newElement, index); |
| return newElement; |
| }, |
| |
| /** |
| * @param {!WebInspector.ElementsTreeElement} treeElement |
| * @param {!WebInspector.ElementsTreeElement} child |
| * @param {number} targetIndex |
| */ |
| _moveChild: function(treeElement, child, targetIndex) |
| { |
| if (treeElement.indexOfChild(child) === targetIndex) |
| return; |
| var wasSelected = child.selected; |
| if (child.parent) |
| child.parent.removeChild(child); |
| treeElement.insertChild(child, targetIndex); |
| if (wasSelected) |
| child.select(); |
| }, |
| |
| /** |
| * @param {!WebInspector.ElementsTreeElement} treeElement |
| */ |
| _innerUpdateChildren: function(treeElement) |
| { |
| if (this._treeElementsBeingUpdated.has(treeElement)) |
| return; |
| |
| this._treeElementsBeingUpdated.add(treeElement); |
| |
| var node = treeElement.node(); |
| var visibleChildren = this._visibleChildren(node); |
| var 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. |
| var existingTreeElements = new Map(); |
| for (var i = treeElement.childCount() - 1; i >= 0; --i) { |
| var existingTreeElement = treeElement.childAt(i); |
| if (!(existingTreeElement instanceof WebInspector.ElementsTreeElement)) { |
| // Remove expand all button and shadow host toolbar. |
| treeElement.removeChildAtIndex(i); |
| continue; |
| } |
| var elementsTreeElement = /** @type {!WebInspector.ElementsTreeElement} */ (existingTreeElement); |
| var existingNode = elementsTreeElement.node(); |
| |
| if (visibleChildrenSet.has(existingNode)) { |
| existingTreeElements.set(existingNode, existingTreeElement); |
| continue; |
| } |
| |
| treeElement.removeChildAtIndex(i); |
| } |
| |
| for (var i = 0; i < visibleChildren.length && i < treeElement.expandedChildrenLimit(); ++i) { |
| var child = visibleChildren[i]; |
| var 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. |
| var newElement = this.insertChildElement(treeElement, child, i); |
| if (this._updateRecordForHighlight(node) && treeElement.expanded) |
| WebInspector.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. |
| var expandedChildCount = treeElement.childCount(); |
| if (visibleChildren.length > expandedChildCount) { |
| var targetButtonIndex = expandedChildCount; |
| if (!treeElement.expandAllButtonElement) |
| treeElement.expandAllButtonElement = this._createExpandAllButtonTreeElement(treeElement); |
| treeElement.insertChild(treeElement.expandAllButtonElement, targetButtonIndex); |
| treeElement.expandAllButtonElement.button.textContent = WebInspector.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 (var distributedNode of node.distributedNodes()) |
| treeElement.appendChild(new WebInspector.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 {!WebInspector.Event} event |
| */ |
| _markersChanged: function(event) |
| { |
| var node = /** @type {!WebInspector.DOMNode} */ (event.data); |
| var treeElement = node[this._treeElementSymbol]; |
| if (treeElement) |
| treeElement.updateDecorations(); |
| }, |
| |
| __proto__: TreeOutline.prototype |
| } |
| |
| /** |
| * @constructor |
| */ |
| WebInspector.ElementsTreeOutline.UpdateRecord = function() |
| { |
| } |
| |
| WebInspector.ElementsTreeOutline.UpdateRecord.prototype = { |
| /** |
| * @param {string} attrName |
| */ |
| attributeModified: function(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: function(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 {!WebInspector.DOMNode} node |
| */ |
| nodeInserted: function(node) |
| { |
| this._hasChangedChildren = true; |
| }, |
| |
| nodeRemoved: function(node) |
| { |
| this._hasChangedChildren = true; |
| this._hasRemovedChildren = true; |
| }, |
| |
| charDataModified: function() |
| { |
| this._charDataModified = true; |
| }, |
| |
| childrenModified: function() |
| { |
| this._hasChangedChildren = true; |
| }, |
| |
| /** |
| * @param {string} attributeName |
| * @return {boolean} |
| */ |
| isAttributeModified: function(attributeName) |
| { |
| return this._modifiedAttributes && this._modifiedAttributes.has(attributeName); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| hasRemovedAttributes: function() |
| { |
| return !!this._removedAttributes && !!this._removedAttributes.size; |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| isCharDataModified: function() |
| { |
| return !!this._charDataModified; |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| hasChangedChildren: function() |
| { |
| return !!this._hasChangedChildren; |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| hasRemovedChildren: function() |
| { |
| return !!this._hasRemovedChildren; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.Renderer} |
| */ |
| WebInspector.ElementsTreeOutline.Renderer = function() |
| { |
| } |
| |
| WebInspector.ElementsTreeOutline.Renderer.prototype = { |
| /** |
| * @override |
| * @param {!Object} object |
| * @return {!Promise.<!Element>} |
| */ |
| render: function(object) |
| { |
| return new Promise(renderPromise); |
| |
| /** |
| * @param {function(!Element)} resolve |
| * @param {function(!Error)} reject |
| */ |
| function renderPromise(resolve, reject) |
| { |
| if (object instanceof WebInspector.DOMNode) { |
| onNodeResolved(/** @type {!WebInspector.DOMNode} */ (object)); |
| } else if (object instanceof WebInspector.DeferredDOMNode) { |
| (/** @type {!WebInspector.DeferredDOMNode} */ (object)).resolve(onNodeResolved); |
| } else if (object instanceof WebInspector.RemoteObject) { |
| var domModel = WebInspector.DOMModel.fromTarget((/** @type {!WebInspector.RemoteObject} */ (object)).target()); |
| if (domModel) |
| domModel.pushObjectAsNodeToFrontend(object, onNodeResolved); |
| else |
| reject(new Error("No dom model for given JS object target found.")); |
| } else { |
| reject(new Error("Can't reveal not a node.")); |
| } |
| |
| /** |
| * @param {?WebInspector.DOMNode} node |
| */ |
| function onNodeResolved(node) |
| { |
| if (!node) { |
| reject(new Error("Could not resolve node.")); |
| return; |
| } |
| var treeOutline = new WebInspector.ElementsTreeOutline(node.domModel(), 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); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @constructor |
| * @extends {TreeElement} |
| * @param {!WebInspector.DOMNodeShortcut} nodeShortcut |
| */ |
| WebInspector.ElementsTreeOutline.ShortcutTreeElement = function(nodeShortcut) |
| { |
| TreeElement.call(this, ""); |
| this.listItemElement.createChild("div", "selection fill"); |
| var title = this.listItemElement.createChild("span", "elements-tree-shortcut-title"); |
| var text = nodeShortcut.nodeName.toLowerCase(); |
| if (nodeShortcut.nodeType === Node.ELEMENT_NODE) |
| text = "<" + text + ">"; |
| title.textContent = "\u21AA " + text; |
| |
| var link = WebInspector.DOMPresentationUtils.linkifyDeferredNodeReference(nodeShortcut.deferredNode); |
| this.listItemElement.createTextChild(" "); |
| link.classList.add("elements-tree-shortcut-link"); |
| link.textContent = WebInspector.UIString("reveal"); |
| this.listItemElement.appendChild(link); |
| this._nodeShortcut = nodeShortcut; |
| } |
| |
| WebInspector.ElementsTreeOutline.ShortcutTreeElement.prototype = { |
| /** |
| * @return {boolean} |
| */ |
| get hovered() |
| { |
| return this._hovered; |
| }, |
| |
| set hovered(x) |
| { |
| if (this._hovered === x) |
| return; |
| this._hovered = x; |
| this.listItemElement.classList.toggle("hovered", x); |
| }, |
| |
| updateSelection: function() |
| { |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| backendNodeId: function() |
| { |
| return this._nodeShortcut.deferredNode.backendNodeId(); |
| }, |
| |
| /** |
| * @override |
| * @param {boolean=} selectedByUser |
| * @return {boolean} |
| */ |
| onselect: function(selectedByUser) |
| { |
| if (!selectedByUser) |
| return true; |
| this._nodeShortcut.deferredNode.highlight(); |
| this._nodeShortcut.deferredNode.resolve(resolved.bind(this)); |
| /** |
| * @param {?WebInspector.DOMNode} node |
| * @this {WebInspector.ElementsTreeOutline.ShortcutTreeElement} |
| */ |
| function resolved(node) |
| { |
| if (node) { |
| this.treeOutline._selectedDOMNode = node; |
| this.treeOutline._selectedNodeChanged(); |
| } |
| } |
| return true; |
| }, |
| |
| __proto__: TreeElement.prototype |
| } |