/* * Copyright (C) 2007, 2008, 2013 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. */ /** * @constructor * @extends {TreeOutline} * @param {boolean=} omitRootDOMNode * @param {boolean=} selectEnabled */ WebInspector.DOMTreeOutline = function(omitRootDOMNode, selectEnabled, showInElementsPanelEnabled) { 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); 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.classList.add(WebInspector.DOMTreeOutline.StyleClassName); this.element.classList.add(WebInspector.SyntaxHighlightedStyleClassName); TreeOutline.call(this, this.element); this._includeRootDOMNode = !omitRootDOMNode; this._selectEnabled = selectEnabled; this._showInElementsPanelEnabled = showInElementsPanelEnabled; this._rootDOMNode = null; this._selectedDOMNode = null; this._eventSupport = new WebInspector.Object(); this._editing = false; this._visible = false; this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true); this._hideElementKeyboardShortcut = new WebInspector.KeyboardShortcut(null, "H", this._hideElement.bind(this), this.element); this._hideElementKeyboardShortcut.implicitlyPreventsDefault = false; WebInspector.showShadowDOMSetting.addEventListener(WebInspector.Setting.Event.Changed, this._showShadowDOMSettingChanged, this); } WebInspector.Object.addConstructorFunctions(WebInspector.DOMTreeOutline); WebInspector.DOMTreeOutline.StyleClassName = "dom-tree-outline"; WebInspector.DOMTreeOutline.Event = { SelectedNodeChanged: "dom-tree-outline-selected-node-changed" } WebInspector.DOMTreeOutline.prototype = { constructor: WebInspector.DOMTreeOutline, wireToDomAgent: function() { this._elementsTreeUpdater = new WebInspector.DOMTreeUpdater(this); }, close: function() { if (this._elementsTreeUpdater) { this._elementsTreeUpdater.close(); this._elementsTreeUpdater = null; } }, setVisible: function(visible, omitFocus) { this._visible = visible; if (!this._visible) return; this._updateModifiedNodes(); if (this._selectedDOMNode) this._revealAndSelectNode(this._selectedDOMNode, omitFocus); }, addEventListener: function(eventType, listener, thisObject) { this._eventSupport.addEventListener(eventType, listener, thisObject); }, removeEventListener: function(eventType, listener, thisObject) { this._eventSupport.removeEventListener(eventType, listener, thisObject); }, 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; }, selectedDOMNode: function() { return this._selectedDOMNode; }, 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. // Note that _revealAndSelectNode will not do anything for a null node. if (!node || this._selectedDOMNode === node) this._selectedNodeChanged(); }, 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.DOMTreeElement(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.DOMTreeElement(node); treeElement.selectable = this._selectEnabled; this.appendChild(treeElement); node = node.nextSibling; } } if (selectedNode) this._revealAndSelectNode(selectedNode, true); }, updateSelection: function() { if (!this.selectedTreeElement) return; var element = this.treeOutline.selectedTreeElement; element.updateSelection(); }, _selectedNodeChanged: function() { this._eventSupport.dispatchEventToListeners(WebInspector.DOMTreeOutline.Event.SelectedNodeChanged); }, findTreeElement: function(node) { function isAncestorNode(ancestor, node) { return ancestor.isAncestor(node); } function parentNode(node) { return node.parentNode; } 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; 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, omitFocus) { if (!node || this._suppressRevealAndSelect) return; var treeElement = this.createTreeElementFor(node); if (!treeElement) return; treeElement.revealAndSelect(omitFocus); }, _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)) { event.preventDefault(); 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.domTreeManager.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.domTreeManager.hideDOMNodeHighlight(); }, _ondragstart: function(event) { var treeElement = this._treeElementFromEvent(event); if (!treeElement) return false; if (!this._isValidDragSourceOrTarget(treeElement)) return false; if (treeElement.representedObject.nodeName() === "BODY" || treeElement.representedObject.nodeName() === "HEAD") return false; event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent); event.dataTransfer.effectAllowed = "copyMove"; this._nodeBeingDragged = treeElement.representedObject; WebInspector.domTreeManager.hideDOMNodeHighlight(); return true; }, _ondragover: function(event) { if (!this._nodeBeingDragged) return false; var treeElement = this._treeElementFromEvent(event); if (!this._isValidDragSourceOrTarget(treeElement)) return false; var node = treeElement.representedObject; while (node) { if (node === this._nodeBeingDragged) 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; }, _isValidDragSourceOrTarget: function(treeElement) { if (!treeElement) return false; var node = treeElement.representedObject; if (!(node instanceof WebInspector.DOMNode)) return false; if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE) return false; return true; }, _ondrop: function(event) { event.preventDefault(); var treeElement = this._treeElementFromEvent(event); if (this._nodeBeingDragged && treeElement) { var parentNode; var anchorNode; if (treeElement._elementCloseTag) { // Drop onto closing tag -> insert as last child. parentNode = treeElement.representedObject; } else { var dragTargetNode = treeElement.representedObject; parentNode = dragTargetNode.parentNode; anchorNode = dragTargetNode; } function callback(error, newNodeId) { if (error) return; this._updateModifiedNodes(); var newNode = WebInspector.domTreeManager.nodeForId(newNodeId); if (newNode) this.selectDOMNode(newNode, true); } this._nodeBeingDragged.moveTo(parentNode, anchorNode, callback.bind(this)); } delete this._nodeBeingDragged; }, _ondragend: function(event) { event.preventDefault(); this._clearDragOverTreeElementMarker(); delete this._nodeBeingDragged; }, _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) return; var contextMenu = new WebInspector.ContextMenu(event); this.populateContextMenu(contextMenu, event); contextMenu.show(); }, populateContextMenu: function(contextMenu, event) { var treeElement = this._treeElementFromEvent(event); if (!treeElement) return false; var tag = event.target.enclosingNodeOrSelfWithClass("html-tag"); var textNode = event.target.enclosingNodeOrSelfWithClass("html-text-node"); var commentNode = event.target.enclosingNodeOrSelfWithClass("html-comment"); var populated = false; if (tag && treeElement._populateTagContextMenu) { if (populated) contextMenu.appendSeparator(); treeElement._populateTagContextMenu(contextMenu, event); populated = true; } else if (textNode && treeElement._populateTextContextMenu) { if (populated) contextMenu.appendSeparator(); treeElement._populateTextContextMenu(contextMenu, textNode); populated = true; } else if (commentNode && treeElement._populateNodeContextMenu) { if (populated) contextMenu.appendSeparator(); treeElement._populateNodeContextMenu(contextMenu, textNode); populated = true; } return populated; }, adjustCollapsedRange: function() { }, _updateModifiedNodes: function() { if (this._elementsTreeUpdater) this._elementsTreeUpdater._updateModifiedNodes(); }, _populateContextMenu: function(contextMenu, domNode) { if (!this._showInElementsPanelEnabled) return; function revealElement() { WebInspector.domTreeManager.inspectElement(domNode.id); } contextMenu.appendSeparator(); contextMenu.appendItem(WebInspector.UIString("Reveal in DOM Tree"), revealElement); }, _showShadowDOMSettingChanged: function(event) { var nodeToSelect = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null; while (nodeToSelect) { if (!nodeToSelect.isInShadowTree()) break; nodeToSelect = nodeToSelect.parentNode; } this.children.forEach(function(child) { child.updateChildren(true); }); if (nodeToSelect) this.selectDOMNode(nodeToSelect); }, _hideElement: function(event, keyboardShortcut) { if (!this.selectedTreeElement || WebInspector.isEditingAnyField()) return; event.preventDefault(); var selectedNode = this.selectedTreeElement.representedObject; console.assert(selectedNode); if (!selectedNode) return; if (selectedNode.nodeType() !== Node.ELEMENT_NODE) return; if (this._togglePending) return; this._togglePending = true; function toggleProperties() { nodeStyles.removeEventListener(WebInspector.DOMNodeStyles.Event.Refreshed, toggleProperties, this); var opacityProperty = nodeStyles.inlineStyle.propertyForName("opacity"); opacityProperty.value = "0"; opacityProperty.important = true; var pointerEventsProperty = nodeStyles.inlineStyle.propertyForName("pointer-events"); pointerEventsProperty.value = "none"; pointerEventsProperty.important = true; if (opacityProperty.enabled && pointerEventsProperty.enabled) { opacityProperty.remove(); pointerEventsProperty.remove(); } else { opacityProperty.add(); pointerEventsProperty.add(); } delete this._togglePending; } var nodeStyles = WebInspector.cssStyleManager.stylesForNode(selectedNode); if (nodeStyles.needsRefresh) { nodeStyles.addEventListener(WebInspector.DOMNodeStyles.Event.Refreshed, toggleProperties, this); nodeStyles.refresh(); } else toggleProperties.call(this); } } WebInspector.DOMTreeOutline.prototype.__proto__ = TreeOutline.prototype;