/* * 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. */ WebInspector.ElementsTreeOutline = function() { this.element = document.createElement("ol"); this.element.addEventListener("mousedown", this._onmousedown.bind(this), false); this.element.addEventListener("dblclick", this._ondblclick.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.rootDOMNode = null; this.focusedDOMNode = null; } WebInspector.ElementsTreeOutline.prototype = { get rootDOMNode() { return this._rootDOMNode; }, set rootDOMNode(x) { if (objectsAreSame(this._rootDOMNode, x)) return; this._rootDOMNode = x; this.update(); }, get focusedDOMNode() { return this._focusedDOMNode; }, set focusedDOMNode(x) { if (objectsAreSame(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 (objectsAreSame(this._focusedDOMNode, x)) { this.focusedNodeChanged(); if (x && !this.suppressSelectHighlight) { InspectorController.highlightDOMNode(x); if ("_restorePreviousHighlightNodeTimeout" in this) clearTimeout(this._restorePreviousHighlightNodeTimeout); function restoreHighlightToHoveredNode() { var hoveredNode = WebInspector.hoveredDOMNode; if (hoveredNode) InspectorController.highlightDOMNode(hoveredNode); else InspectorController.hideDOMNodeHighlight(); } this._restorePreviousHighlightNodeTimeout = setTimeout(restoreHighlightToHoveredNode, 2000); } } }, update: function() { 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 = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.rootDOMNode) : this.rootDOMNode.firstChild); while (node) { treeElement = new WebInspector.ElementsTreeElement(node); treeElement.selectable = this.selectEnabled; this.appendChild(treeElement); node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling; } } this.updateSelection(); }, updateSelection: function() { if (!this.selectedTreeElement) return; var element = this.treeOutline.selectedTreeElement; element.updateSelection(); }, focusedNodeChanged: function(forceUpdate) {}, findTreeElement: function(node, isAncestor, getParent, equal) { if (typeof isAncestor === "undefined") isAncestor = isAncestorIncludingParentFrames; if (typeof getParent === "undefined") getParent = parentNodeOrFrameElement; if (typeof equal === "undefined") equal = objectsAreSame; var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestor, getParent, equal); 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, isAncestor, getParent, equal); } return treeElement; }, revealAndSelectNode: function(node) { if (!node) return; var treeElement = this.findTreeElement(node); if (!treeElement) return; treeElement.reveal(); treeElement.select(); }, _treeElementFromEvent: function(event) { var root = this.element; // We choose this X coordinate based on the knowledge that our list // items extend nearly to the right edge of the outer <ol>. var x = root.totalOffsetLeft + root.offsetWidth - 20; 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; }, _ondblclick: function(event) { var element = this._treeElementFromEvent(event); if (!element || !element.ondblclick) return; element.ondblclick(element, event); }, _onmousedown: function(event) { var element = this._treeElementFromEvent(event); if (!element || element.isEventWithinDisclosureTriangle(event)) return; element.select(); }, _onmousemove: function(event) { if (this._previousHoveredElement) { this._previousHoveredElement.hovered = false; delete this._previousHoveredElement; } var element = this._treeElementFromEvent(event); if (element && !element.elementCloseTag) { element.hovered = true; this._previousHoveredElement = element; } WebInspector.hoveredDOMNode = (element && !element.elementCloseTag ? element.representedObject : null); }, _onmouseout: function(event) { var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); if (nodeUnderMouse.isDescendant(this.element)) return; if (this._previousHoveredElement) { this._previousHoveredElement.hovered = false; delete this._previousHoveredElement; } WebInspector.hoveredDOMNode = null; } } WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype; WebInspector.ElementsTreeElement = function(node) { var hasChildren = node.contentDocument || (Preferences.ignoreWhitespace ? (firstChildSkippingWhitespace.call(node) ? true : false) : node.hasChildNodes()); var titleInfo = nodeTitleInfo.call(node, hasChildren, WebInspector.linkifyURL); if (titleInfo.hasChildren) this.whitespaceIgnored = Preferences.ignoreWhitespace; // The title will be updated in onattach. TreeElement.call(this, "", node, titleInfo.hasChildren); if (this.representedObject.nodeType == Node.ELEMENT_NODE) this._canAddAttributes = true; } WebInspector.ElementsTreeElement.prototype = { get highlighted() { return this._highlighted; }, set highlighted(x) { if (this._highlighted === x) return; this._highlighted = x; if (this.listItemElement) { if (x) this.listItemElement.addStyleClass("highlighted"); else this.listItemElement.removeStyleClass("highlighted"); } }, 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"); if (this._canAddAttributes) this.toggleNewAttributeButton(); } }, toggleNewAttributeButton: function() { function removeWhenEditing(event) { if (this._addAttributeElement && this._addAttributeElement.parentNode) this._addAttributeElement.parentNode.removeChild(this._addAttributeElement); delete this._addAttributeElement; } if (!this._addAttributeElement && this._hovered && !this._editing) { var span = document.createElement("span"); span.className = "add-attribute"; span.textContent = "\u2026"; span.addEventListener("dblclick", removeWhenEditing.bind(this), false); this._addAttributeElement = span; var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0]; this._insertInLastAttributePosition(tag, span); } else if (!this._hovered && this._addAttributeElement) { if (this._addAttributeElement.parentNode) this._addAttributeElement.parentNode.removeChild(this._addAttributeElement); delete this._addAttributeElement; } }, 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() { this.listItemElement.addEventListener("mousedown", this.onmousedown.bind(this), false); if (this._highlighted) this.listItemElement.addStyleClass("highlighted"); 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.whitespaceIgnored !== Preferences.ignoreWhitespace) return; this.whitespaceIgnored = Preferences.ignoreWhitespace; this.updateChildren(); }, updateChildren: function(fullRefresh) { if (fullRefresh) { var selectedTreeElement = this.treeOutline.selectedTreeElement; if (selectedTreeElement && selectedTreeElement.hasAncestor(this)) this.select(); this.removeChildren(); } var treeElement = this; var treeChildIndex = 0; function updateChildrenOfNode(node) { var treeOutline = treeElement.treeOutline; var child = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(node) : node.firstChild); while (child) { var currentTreeElement = treeElement.children[treeChildIndex]; if (!currentTreeElement || !objectsAreSame(currentTreeElement.representedObject, child)) { // Find any existing element that is later in the children list. var existingTreeElement = null; for (var i = (treeChildIndex + 1); i < treeElement.children.length; ++i) { if (objectsAreSame(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. var wasSelected = existingTreeElement.selected; treeElement.removeChild(existingTreeElement); treeElement.insertChild(existingTreeElement, treeChildIndex); if (wasSelected) existingTreeElement.select(); } else { // No existing element found, insert a new element. var newElement = new WebInspector.ElementsTreeElement(child); newElement.selectable = treeOutline.selectEnabled; treeElement.insertChild(newElement, treeChildIndex); } } child = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(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) { if ("elementCloseTag" in this.children[i]) continue; var currentChild = this.children[i]; var currentNode = currentChild.representedObject; var currentParentNode = currentNode.parentNode; if (objectsAreSame(currentParentNode, this.representedObject)) continue; if (this.representedObject.contentDocument && objectsAreSame(currentParentNode, this.representedObject.contentDocument)) continue; var selectedTreeElement = this.treeOutline.selectedTreeElement; if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild))) this.select(); this.removeChildAtIndex(i); if (this.treeOutline.panel && currentNode.contentDocument) this.treeOutline.panel.unregisterMutationEventListeners(currentNode.contentDocument.defaultView); } if (this.representedObject.contentDocument) updateChildrenOfNode(this.representedObject.contentDocument); updateChildrenOfNode(this.representedObject); var lastChild = this.children[this.children.length - 1]; if (this.representedObject.nodeType == Node.ELEMENT_NODE && (!lastChild || !lastChild.elementCloseTag)) { var title = "<span class=\"webkit-html-tag close\"></" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "></span>"; var item = new TreeElement(title, null, false); item.selectable = false; item.elementCloseTag = true; this.appendChild(item); } }, onexpand: function() { this.treeOutline.updateSelection(); if (this.treeOutline.panel && this.representedObject.contentDocument) this.treeOutline.panel.registerMutationEventListeners(this.representedObject.contentDocument.defaultView); }, oncollapse: function() { this.treeOutline.updateSelection(); }, onreveal: function() { if (this.listItemElement) this.listItemElement.scrollIntoViewIfNeeded(false); }, onselect: function() { this.treeOutline.focusedDOMNode = this.representedObject; this.updateSelection(); }, onmousedown: function(event) { if (this._editing) return; // Prevent selecting the nearest word on double click. if (event.detail >= 2) event.preventDefault(); }, ondblclick: function(treeElement, event) { if (this._editing) return; if (this._startEditing(event, treeElement)) return; if (this.treeOutline.panel) { this.treeOutline.rootDOMNode = this.representedObject.parentNode; this.treeOutline.focusedDOMNode = this.representedObject; } if (this.hasChildren && !this.expanded) this.expand(); }, _insertInLastAttributePosition: function(tag, node) { if (tag.getElementsByClassName("webkit-html-attribute").length > 0) tag.insertBefore(node, tag.lastChild); else { var nodeName = tag.textContent.match(/^<(.*?)>$/)[1]; tag.textContent = ''; tag.appendChild(document.createTextNode('<'+nodeName)); tag.appendChild(node); tag.appendChild(document.createTextNode('>')); } }, _startEditing: function(event, treeElement) { if (this.treeOutline.focusedDOMNode != this.representedObject) return; if (this.representedObject.nodeType != Node.ELEMENT_NODE && this.representedObject.nodeType != Node.TEXT_NODE) return false; var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node"); if (textNode) return this._startEditingTextNode(textNode); var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute"); if (attribute) return this._startEditingAttribute(attribute, event.target); var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute"); if (newAttribute) return this._addNewAttribute(treeElement.listItemElement); return false; }, _addNewAttribute: function(listItemElement) { var attr = document.createElement("span"); attr.className = "webkit-html-attribute"; attr.style.marginLeft = "2px"; // overrides the .editing margin rule attr.style.marginRight = "2px"; // overrides the .editing margin rule var name = document.createElement("span"); name.className = "webkit-html-attribute-name new-attribute"; name.textContent = " "; var value = document.createElement("span"); value.className = "webkit-html-attribute-value"; attr.appendChild(name); attr.appendChild(value); var tag = listItemElement.getElementsByClassName("webkit-html-tag")[0]; this._insertInLastAttributePosition(tag, attr); return this._startEditingAttribute(attr, attr); }, _triggerEditAttribute: function(attributeName) { var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name"); for (var i = 0, len = attributeElements.length; i < len; ++i) { if (attributeElements[i].textContent === attributeName) { for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) { if (elem.nodeType !== Node.ELEMENT_NODE) continue; if (elem.hasStyleClass("webkit-html-attribute-value")) return this._startEditingAttribute(attributeElements[i].parentNode, elem); } } } }, _startEditingAttribute: function(attribute, elementForSelection) { if (WebInspector.isBeingEdited(attribute)) return true; var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0]; if (!attributeNameElement) return false; var attributeName = attributeNameElement.innerText; function removeZeroWidthSpaceRecursive(node) { if (node.nodeType === Node.TEXT_NODE) { node.nodeValue = node.nodeValue.replace(/\u200B/g, ""); return; } if (node.nodeType !== Node.ELEMENT_NODE) return; for (var child = node.firstChild; child; child = child.nextSibling) removeZeroWidthSpaceRecursive(child); } // Remove zero-width spaces that were added by nodeTitleInfo. removeZeroWidthSpaceRecursive(attribute); this._editing = true; WebInspector.startEditing(attribute, this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName); window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1); return true; }, _startEditingTextNode: function(textNode) { if (WebInspector.isBeingEdited(textNode)) return true; this._editing = true; WebInspector.startEditing(textNode, this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this)); window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1); return true; }, _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection) { delete this._editing; // Before we do anything, determine where we should move // next based on the current element's settings var moveToAttribute; var newAttribute; if (moveDirection) { var found = false; var attributes = this.representedObject.attributes; for (var i = 0, len = attributes.length; i < len; ++i) { if (attributes[i].name === attributeName) { found = true; if (moveDirection === "backward" && i > 0) moveToAttribute = attributes[i - 1].name; else if (moveDirection === "forward" && i < attributes.length - 1) moveToAttribute = attributes[i + 1].name; else if (moveDirection === "forward" && i === attributes.length - 1) newAttribute = true; } } if (!found && moveDirection === "backward") moveToAttribute = attributes[attributes.length - 1].name; else if (!found && moveDirection === "forward" && !/^\s*$/.test(newText)) newAttribute = true; } function moveToNextAttributeIfNeeded() { if (moveToAttribute) this._triggerEditAttribute(moveToAttribute); else if (newAttribute) this._addNewAttribute(this.listItemElement); } var parseContainerElement = document.createElement("span"); parseContainerElement.innerHTML = "<span " + newText + "></span>"; var parseElement = parseContainerElement.firstChild; if (!parseElement) { this._editingCancelled(element, attributeName); moveToNextAttributeIfNeeded.call(this); return; } if (!parseElement.hasAttributes()) { InspectorController.inspectedWindow().Element.prototype.removeAttribute.call(this.representedObject, attributeName); this._updateTitle(); moveToNextAttributeIfNeeded.call(this); return; } var foundOriginalAttribute = false; for (var i = 0; i < parseElement.attributes.length; ++i) { var attr = parseElement.attributes[i]; foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName; try { InspectorController.inspectedWindow().Element.prototype.setAttribute.call(this.representedObject, attr.name, attr.value); } catch(e) {} // ignore invalid attribute (innerHTML doesn't throw errors, but this can) } if (!foundOriginalAttribute) InspectorController.inspectedWindow().Element.prototype.removeAttribute.call(this.representedObject, attributeName); this._updateTitle(); this.treeOutline.focusedNodeChanged(true); moveToNextAttributeIfNeeded.call(this); }, _textNodeEditingCommitted: function(element, newText) { delete this._editing; var textNode; if (this.representedObject.nodeType == Node.ELEMENT_NODE) { // We only show text nodes inline in elements if the element only // has a single child, and that child is a text node. textNode = this.representedObject.firstChild; } else if (this.representedObject.nodeType == Node.TEXT_NODE) textNode = this.representedObject; textNode.nodeValue = newText; this._updateTitle(); }, _editingCancelled: function(element, context) { delete this._editing; this._updateTitle(); }, _updateTitle: function() { var title = nodeTitleInfo.call(this.representedObject, this.hasChildren, WebInspector.linkifyURL).title; this.title = "<span class=\"highlight\">" + title + "</span>"; delete this.selectionElement; this.updateSelection(); this._preventFollowingLinksOnDoubleClick(); }, } WebInspector.ElementsTreeElement.prototype.__proto__ = TreeElement.prototype;