/* * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2008 Matt Lilek * 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. */ WebInspector.ElementsTreeOutline = function() { this.element = document.createElement("ol"); this.element.addEventListener("mousedown", this._onmousedown.bind(this), false); this.element.addEventListener("mousemove", this._onmousemove.bind(this), false); this.element.addEventListener("mouseout", this._onmouseout.bind(this), false); TreeOutline.call(this, this.element); this.includeRootDOMNode = true; this.selectEnabled = false; this.showInElementsPanelEnabled = false; this.rootDOMNode = null; this.focusedDOMNode = null; } WebInspector.ElementsTreeOutline.prototype = { get rootDOMNode() { return this._rootDOMNode; }, set rootDOMNode(x) { if (this._rootDOMNode === x) return; this._rootDOMNode = x; this._isXMLMimeType = !!(WebInspector.mainResource && WebInspector.mainResource.mimeType && WebInspector.mainResource.mimeType.match(/x(?:ht)?ml/i)); this.update(); }, get isXMLMimeType() { return this._isXMLMimeType; }, nodeNameToCorrectCase: function(nodeName) { return this.isXMLMimeType ? nodeName : nodeName.toLowerCase(); }, get focusedDOMNode() { return this._focusedDOMNode; }, set focusedDOMNode(x) { if (this._focusedDOMNode === x) { this.revealAndSelectNode(x); return; } this._focusedDOMNode = x; this.revealAndSelectNode(x); // The revealAndSelectNode() method might find a different element if there is inlined text, // and the select() call would change the focusedDOMNode and reenter this setter. So to // avoid calling focusedNodeChanged() twice, first check if _focusedDOMNode is the same // node as the one passed in. if (this._focusedDOMNode === x) this.focusedNodeChanged(); }, get editing() { return this._editing; }, update: function() { var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null; this.removeChildren(); if (!this.rootDOMNode) return; var treeElement; if (this.includeRootDOMNode) { treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode); treeElement.selectable = this.selectEnabled; 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 = new WebInspector.ElementsTreeElement(node); treeElement.selectable = this.selectEnabled; this.appendChild(treeElement); node = node.nextSibling; } } if (selectedNode) this.revealAndSelectNode(selectedNode); }, updateSelection: function() { if (!this.selectedTreeElement) return; var element = this.treeOutline.selectedTreeElement; element.updateSelection(); }, focusedNodeChanged: function(forceUpdate) {}, findTreeElement: function(node) { var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode); 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 = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode); } return treeElement; }, createTreeElementFor: function(node) { var treeElement = this.findTreeElement(node); if (treeElement) return treeElement; if (!node.parentNode) return null; var treeElement = this.createTreeElementFor(node.parentNode); if (treeElement && treeElement.showChild(node.index)) return treeElement.children[node.index]; return null; }, set suppressRevealAndSelect(x) { if (this._suppressRevealAndSelect === x) return; this._suppressRevealAndSelect = x; }, revealAndSelectNode: function(node) { if (!node || this._suppressRevealAndSelect) return; var treeElement = this.createTreeElementFor(node); if (!treeElement) return; treeElement.reveal(); treeElement.select(); }, _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
    container. // In the no-word-wrap mode the outer
      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; }, _onmousedown: function(event) { var element = this._treeElementFromEvent(event); if (!element || element.isEventWithinDisclosureTriangle(event)) return; element.select(); }, _onmousemove: function(event) { var element = this._treeElementFromEvent(event); if (element && this._previousHoveredElement === element) return; if (this._previousHoveredElement) { this._previousHoveredElement.hovered = false; delete this._previousHoveredElement; } if (element) { element.hovered = true; this._previousHoveredElement = element; // Lazily compute tag-specific tooltips. if (element.representedObject && !element.tooltip) element._createTooltipForNode(); } WebInspector.highlightDOMNode(element ? element.representedObject.id : 0); }, _onmouseout: function(event) { var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element)) return; if (this._previousHoveredElement) { this._previousHoveredElement.hovered = false; delete this._previousHoveredElement; } WebInspector.highlightDOMNode(0); }, populateContextMenu: function(contextMenu, event) { var listItem = event.target.enclosingNodeOrSelfWithNodeName("LI"); if (!listItem || !listItem.treeElement) return false; var populated; if (this.showInElementsPanelEnabled) { function focusElement() { WebInspector.panels.elements.switchToAndFocus(listItem.treeElement.representedObject); } contextMenu.appendItem(WebInspector.UIString("Reveal in Elements Panel"), focusElement.bind(this)); populated = true; } else { var href = event.target.enclosingNodeOrSelfWithClass("webkit-html-resource-link") || event.target.enclosingNodeOrSelfWithClass("webkit-html-external-link"); var tag = event.target.enclosingNodeOrSelfWithClass("webkit-html-tag"); var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node"); if (href) populated = WebInspector.panels.elements.populateHrefContextMenu(contextMenu, event, href); if (tag && listItem.treeElement._populateTagContextMenu) { if (populated) contextMenu.appendSeparator(); listItem.treeElement._populateTagContextMenu(contextMenu, event); populated = true; } else if (textNode && listItem.treeElement._populateTextContextMenu) { if (populated) contextMenu.appendSeparator(); listItem.treeElement._populateTextContextMenu(contextMenu, textNode); populated = true; } } return populated; } } WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype; WebInspector.ElementsTreeElement = function(node, elementCloseTag) { this._elementCloseTag = elementCloseTag; var hasChildrenOverride = !elementCloseTag && node.hasChildNodes() && !this._showInlineText(node); // The title will be updated in onattach. TreeElement.call(this, "", node, hasChildrenOverride); if (this.representedObject.nodeType() == Node.ELEMENT_NODE && !elementCloseTag) this._canAddAttributes = true; this._searchQuery = null; this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit; } WebInspector.ElementsTreeElement.InitialChildrenLimit = 500; // A union of HTML4 and HTML5-Draft elements that explicitly // or implicitly (for HTML5) forbid the closing tag. // FIXME: Revise once HTML5 Final is published. WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [ "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source" ].keySet(); // These tags we do not allow editing their tag name. WebInspector.ElementsTreeElement.EditTagBlacklist = [ "html", "head", "body" ].keySet(); WebInspector.ElementsTreeElement.prototype = { highlightSearchResults: function(searchQuery) { if (this._searchQuery === searchQuery) return; if (searchQuery) delete this._searchHighlightedHTML; // A new search query (not clear-the-current-highlighting). this._searchQuery = searchQuery; this.updateTitle(true); }, get hovered() { return this._hovered; }, set hovered(x) { if (this._hovered === x) return; this._hovered = x; if (this.listItemElement) { if (x) { this.updateSelection(); this.listItemElement.addStyleClass("hovered"); } else { this.listItemElement.removeStyleClass("hovered"); } } }, get expandedChildrenLimit() { return this._expandedChildrenLimit; }, set expandedChildrenLimit(x) { if (this._expandedChildrenLimit === x) return; this._expandedChildrenLimit = x; if (this.treeOutline && !this._updateChildrenInProgress) this._updateChildren(true); }, get expandedChildCount() { var count = this.children.length; if (count && this.children[count - 1]._elementCloseTag) count--; if (count && this.children[count - 1].expandAllButton) count--; return count; }, showChild: function(index) { if (this._elementCloseTag) return; if (index >= this.expandedChildrenLimit) { this._expandedChildrenLimit = index + 1; this._updateChildren(true); } // Whether index-th child is visible in the children tree return this.expandedChildCount > index; }, _createTooltipForNode: function() { var node = this.representedObject; if (!node.nodeName() || node.nodeName().toLowerCase() !== "img") return; function setTooltip(error, result) { if (error || !result || result.type !== "string") return; try { var properties = JSON.parse(result.description); var offsetWidth = properties[0]; var offsetHeight = properties[1]; var naturalWidth = properties[2]; var naturalHeight = properties[3]; if (offsetHeight === naturalHeight && offsetWidth === naturalWidth) this.tooltip = WebInspector.UIString("%d \xd7 %d pixels", offsetWidth, offsetHeight); else this.tooltip = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)", offsetWidth, offsetHeight, naturalWidth, naturalHeight); } catch (e) { console.error(e); } } function resolvedNode(object) { if (!object) return; object.evaluate("return '[' + this.offsetWidth + ',' + this.offsetHeight + ',' + this.naturalWidth + ',' + this.naturalHeight + ']'", setTooltip.bind(this)); object.release(); } WebInspector.RemoteObject.resolveNode(node, resolvedNode.bind(this)); }, updateSelection: function() { var listItemElement = this.listItemElement; if (!listItemElement) return; if (document.body.offsetWidth <= 0) { // The stylesheet hasn't loaded yet or the window is closed, // so we can't calculate what is need. Return early. return; } if (!this.selectionElement) { this.selectionElement = document.createElement("div"); this.selectionElement.className = "selection selected"; listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild); } this.selectionElement.style.height = listItemElement.offsetHeight + "px"; }, onattach: function() { if (this._hovered) { this.updateSelection(); this.listItemElement.addStyleClass("hovered"); } this.updateTitle(); this._preventFollowingLinksOnDoubleClick(); }, _preventFollowingLinksOnDoubleClick: function() { var links = this.listItemElement.querySelectorAll("li > .webkit-html-tag > .webkit-html-attribute > .webkit-html-external-link, li > .webkit-html-tag > .webkit-html-attribute > .webkit-html-resource-link"); if (!links) return; for (var i = 0; i < links.length; ++i) links[i].preventFollowOnDoubleClick = true; }, onpopulate: function() { if (this.children.length || this._showInlineText(this.representedObject) || this._elementCloseTag) return; this.updateChildren(); }, updateChildren: function(fullRefresh) { if (this._elementCloseTag) return; this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh)); }, insertChildElement: function(child, index, closingTag) { var newElement = new WebInspector.ElementsTreeElement(child, closingTag); newElement.selectable = this.treeOutline.selectEnabled; this.insertChild(newElement, index); return newElement; }, moveChild: function(child, targetIndex) { var wasSelected = child.selected; this.removeChild(child); this.insertChild(child, targetIndex); if (wasSelected) child.select(); }, _updateChildren: function(fullRefresh) { if (this._updateChildrenInProgress) return; this._updateChildrenInProgress = true; var focusedNode = this.treeOutline.focusedDOMNode; var originalScrollTop; if (fullRefresh) { var treeOutlineContainerElement = this.treeOutline.element.parentNode; originalScrollTop = treeOutlineContainerElement.scrollTop; var selectedTreeElement = this.treeOutline.selectedTreeElement; if (selectedTreeElement && selectedTreeElement.hasAncestor(this)) this.select(); this.removeChildren(); } var treeElement = this; var treeChildIndex = 0; var elementToSelect; function updateChildrenOfNode(node) { var treeOutline = treeElement.treeOutline; var child = node.firstChild; while (child) { var currentTreeElement = treeElement.children[treeChildIndex]; if (!currentTreeElement || currentTreeElement.representedObject !== child) { // Find any existing element that is later in the children list. var existingTreeElement = null; for (var i = (treeChildIndex + 1), size = treeElement.expandedChildCount; i < size; ++i) { if (treeElement.children[i].representedObject === child) { existingTreeElement = treeElement.children[i]; break; } } if (existingTreeElement && existingTreeElement.parent === treeElement) { // If an existing element was found and it has the same parent, just move it. treeElement.moveChild(existingTreeElement, treeChildIndex); } else { // No existing element found, insert a new element. if (treeChildIndex < treeElement.expandedChildrenLimit) { var newElement = treeElement.insertChildElement(child, treeChildIndex); if (child === focusedNode) elementToSelect = newElement; if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit) treeElement.expandedChildrenLimit++; } } } child = child.nextSibling; ++treeChildIndex; } } // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent. for (var i = (this.children.length - 1); i >= 0; --i) { var currentChild = this.children[i]; var currentNode = currentChild.representedObject; var currentParentNode = currentNode.parentNode; if (currentParentNode === this.representedObject) continue; var selectedTreeElement = this.treeOutline.selectedTreeElement; if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild))) this.select(); this.removeChildAtIndex(i); } updateChildrenOfNode(this.representedObject); this.adjustCollapsedRange(false); var lastChild = this.children[this.children.length - 1]; if (this.representedObject.nodeType() == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag)) this.insertChildElement(this.representedObject, this.children.length, true); // We want to restore the original selection and tree scroll position after a full refresh, if possible. if (fullRefresh && elementToSelect) { elementToSelect.select(); if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight) treeOutlineContainerElement.scrollTop = originalScrollTop; } delete this._updateChildrenInProgress; }, adjustCollapsedRange: function() { // Ensure precondition: only the tree elements for node children are found in the tree // (not the Expand All button or the closing tag). if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent) this.removeChild(this.expandAllButtonElement.__treeElement); const node = this.representedObject; if (!node.children) return; const childNodeCount = node.children.length; // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom. for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i) this.insertChildElement(node.children[i], i); const expandedChildCount = this.expandedChildCount; if (childNodeCount > this.expandedChildCount) { var targetButtonIndex = expandedChildCount; if (!this.expandAllButtonElement) { var item = new TreeElement(null, null, false); item.titleHTML = "