Home | History | Annotate | Download | only in front-end
      1 /*
      2  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
      3  * Copyright (C) 2008 Matt Lilek <webkit (at) mattlilek.com>
      4  * Copyright (C) 2009 Joseph Pecoraro
      5  *
      6  * Redistribution and use in source and binary forms, with or without
      7  * modification, are permitted provided that the following conditions
      8  * are met:
      9  *
     10  * 1.  Redistributions of source code must retain the above copyright
     11  *     notice, this list of conditions and the following disclaimer.
     12  * 2.  Redistributions in binary form must reproduce the above copyright
     13  *     notice, this list of conditions and the following disclaimer in the
     14  *     documentation and/or other materials provided with the distribution.
     15  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
     16  *     its contributors may be used to endorse or promote products derived
     17  *     from this software without specific prior written permission.
     18  *
     19  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
     20  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     21  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     22  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
     23  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     24  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     28  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 WebInspector.ElementsTreeOutline = function() {
     32     this.element = document.createElement("ol");
     33     this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
     34     this.element.addEventListener("mousemove", this._onmousemove.bind(this), false);
     35     this.element.addEventListener("mouseout", this._onmouseout.bind(this), false);
     36 
     37     TreeOutline.call(this, this.element);
     38 
     39     this.includeRootDOMNode = true;
     40     this.selectEnabled = false;
     41     this.showInElementsPanelEnabled = false;
     42     this.rootDOMNode = null;
     43     this.focusedDOMNode = null;
     44 
     45     this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
     46     this.element.addEventListener("keydown", this._keyDown.bind(this), true);
     47 }
     48 
     49 WebInspector.ElementsTreeOutline.prototype = {
     50     get rootDOMNode()
     51     {
     52         return this._rootDOMNode;
     53     },
     54 
     55     set rootDOMNode(x)
     56     {
     57         if (this._rootDOMNode === x)
     58             return;
     59 
     60         this._rootDOMNode = x;
     61 
     62         this.update();
     63     },
     64 
     65     get focusedDOMNode()
     66     {
     67         return this._focusedDOMNode;
     68     },
     69 
     70     set focusedDOMNode(x)
     71     {
     72         if (this._focusedDOMNode === x) {
     73             this.revealAndSelectNode(x);
     74             return;
     75         }
     76 
     77         this._focusedDOMNode = x;
     78 
     79         this.revealAndSelectNode(x);
     80 
     81         // The revealAndSelectNode() method might find a different element if there is inlined text,
     82         // and the select() call would change the focusedDOMNode and reenter this setter. So to
     83         // avoid calling focusedNodeChanged() twice, first check if _focusedDOMNode is the same
     84         // node as the one passed in.
     85         if (this._focusedDOMNode === x) {
     86             this.focusedNodeChanged();
     87 
     88             if (x && !this.suppressSelectHighlight) {
     89                 InspectorBackend.highlightDOMNode(x.id);
     90 
     91                 if ("_restorePreviousHighlightNodeTimeout" in this)
     92                     clearTimeout(this._restorePreviousHighlightNodeTimeout);
     93 
     94                 function restoreHighlightToHoveredNode()
     95                 {
     96                     var hoveredNode = WebInspector.hoveredDOMNode;
     97                     if (hoveredNode)
     98                         InspectorBackend.highlightDOMNode(hoveredNode.id);
     99                     else
    100                         InspectorBackend.hideDOMNodeHighlight();
    101                 }
    102 
    103                 this._restorePreviousHighlightNodeTimeout = setTimeout(restoreHighlightToHoveredNode, 2000);
    104             }
    105         }
    106     },
    107 
    108     update: function()
    109     {
    110         var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
    111 
    112         this.removeChildren();
    113 
    114         if (!this.rootDOMNode)
    115             return;
    116 
    117         var treeElement;
    118         if (this.includeRootDOMNode) {
    119             treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode);
    120             treeElement.selectable = this.selectEnabled;
    121             this.appendChild(treeElement);
    122         } else {
    123             // FIXME: this could use findTreeElement to reuse a tree element if it already exists
    124             var node = this.rootDOMNode.firstChild;
    125             while (node) {
    126                 treeElement = new WebInspector.ElementsTreeElement(node);
    127                 treeElement.selectable = this.selectEnabled;
    128                 this.appendChild(treeElement);
    129                 node = node.nextSibling;
    130             }
    131         }
    132 
    133         if (selectedNode)
    134             this.revealAndSelectNode(selectedNode);
    135     },
    136 
    137     updateSelection: function()
    138     {
    139         if (!this.selectedTreeElement)
    140             return;
    141         var element = this.treeOutline.selectedTreeElement;
    142         element.updateSelection();
    143     },
    144 
    145     focusedNodeChanged: function(forceUpdate) {},
    146 
    147     findTreeElement: function(node)
    148     {
    149         var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode);
    150         if (!treeElement && node.nodeType === Node.TEXT_NODE) {
    151             // The text node might have been inlined if it was short, so try to find the parent element.
    152             treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode);
    153         }
    154 
    155         return treeElement;
    156     },
    157 
    158     revealAndSelectNode: function(node)
    159     {
    160         if (!node)
    161             return;
    162 
    163         var treeElement = this.findTreeElement(node);
    164         if (!treeElement)
    165             return;
    166 
    167         treeElement.reveal();
    168         treeElement.select();
    169     },
    170 
    171     _treeElementFromEvent: function(event)
    172     {
    173         var root = this.element;
    174 
    175         // We choose this X coordinate based on the knowledge that our list
    176         // items extend nearly to the right edge of the outer <ol>.
    177         var x = root.totalOffsetLeft + root.offsetWidth - 20;
    178 
    179         var y = event.pageY;
    180 
    181         // Our list items have 1-pixel cracks between them vertically. We avoid
    182         // the cracks by checking slightly above and slightly below the mouse
    183         // and seeing if we hit the same element each time.
    184         var elementUnderMouse = this.treeElementFromPoint(x, y);
    185         var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
    186         var element;
    187         if (elementUnderMouse === elementAboveMouse)
    188             element = elementUnderMouse;
    189         else
    190             element = this.treeElementFromPoint(x, y + 2);
    191 
    192         return element;
    193     },
    194 
    195     _keyDown: function(event)
    196     {
    197         if (event.target !== this.treeOutline.element)
    198             return;
    199 
    200         var selectedElement = this.selectedTreeElement;
    201         if (!selectedElement)
    202             return;
    203 
    204         if (event.keyCode === WebInspector.KeyboardShortcut.KeyCodes.Backspace ||
    205                 event.keyCode === WebInspector.KeyboardShortcut.KeyCodes.Delete) {
    206             selectedElement.remove();
    207             event.preventDefault();
    208             event.stopPropagation();
    209             return;
    210         }
    211 
    212         // On Enter or Return start editing the first attribute
    213         // or create a new attribute on the selected element.
    214         if (isEnterKey(event)) {
    215             if (this._editing)
    216                 return;
    217 
    218             selectedElement._startEditing();
    219 
    220             // prevent a newline from being immediately inserted
    221             event.preventDefault();
    222             event.stopPropagation();
    223             return;
    224         }
    225     },
    226 
    227     _onmousedown: function(event)
    228     {
    229         var element = this._treeElementFromEvent(event);
    230 
    231         if (!element || element.isEventWithinDisclosureTriangle(event))
    232             return;
    233 
    234         element.select();
    235     },
    236 
    237     _onmousemove: function(event)
    238     {
    239         var element = this._treeElementFromEvent(event);
    240         if (element && this._previousHoveredElement === element)
    241             return;
    242 
    243         if (this._previousHoveredElement) {
    244             this._previousHoveredElement.hovered = false;
    245             delete this._previousHoveredElement;
    246         }
    247 
    248         if (element && !element.elementCloseTag) {
    249             element.hovered = true;
    250             this._previousHoveredElement = element;
    251         }
    252 
    253         WebInspector.hoveredDOMNode = (element && !element.elementCloseTag ? element.representedObject : null);
    254     },
    255 
    256     _onmouseout: function(event)
    257     {
    258         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
    259         if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
    260             return;
    261 
    262         if (this._previousHoveredElement) {
    263             this._previousHoveredElement.hovered = false;
    264             delete this._previousHoveredElement;
    265         }
    266 
    267         WebInspector.hoveredDOMNode = null;
    268     },
    269 
    270     _contextMenuEventFired: function(event)
    271     {
    272         var listItem = event.target.enclosingNodeOrSelfWithNodeName("LI");
    273         if (!listItem || !listItem.treeElement)
    274             return;
    275 
    276         var contextMenu = new WebInspector.ContextMenu();
    277 
    278         var tag = event.target.enclosingNodeOrSelfWithClass("webkit-html-tag");
    279         var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
    280         if (tag)
    281             listItem.treeElement._populateTagContextMenu(contextMenu, event);
    282         else if (textNode)
    283             listItem.treeElement._populateTextContextMenu(contextMenu, textNode);
    284         contextMenu.show(event);
    285     }
    286 }
    287 
    288 WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype;
    289 
    290 WebInspector.ElementsTreeElement = function(node)
    291 {
    292     var hasChildrenOverride = node.hasChildNodes() && !this._showInlineText(node);
    293 
    294     // The title will be updated in onattach.
    295     TreeElement.call(this, "", node, hasChildrenOverride);
    296 
    297     if (this.representedObject.nodeType == Node.ELEMENT_NODE)
    298         this._canAddAttributes = true;
    299 }
    300 
    301 WebInspector.ElementsTreeElement.prototype = {
    302     get highlighted()
    303     {
    304         return this._highlighted;
    305     },
    306 
    307     set highlighted(x)
    308     {
    309         if (this._highlighted === x)
    310             return;
    311 
    312         this._highlighted = x;
    313 
    314         if (this.listItemElement) {
    315             if (x)
    316                 this.listItemElement.addStyleClass("highlighted");
    317             else
    318                 this.listItemElement.removeStyleClass("highlighted");
    319         }
    320     },
    321 
    322     get hovered()
    323     {
    324         return this._hovered;
    325     },
    326 
    327     set hovered(x)
    328     {
    329         if (this._hovered === x)
    330             return;
    331 
    332         this._hovered = x;
    333 
    334         if (this.listItemElement) {
    335             if (x) {
    336                 this.updateSelection();
    337                 this.listItemElement.addStyleClass("hovered");
    338             } else {
    339                 this.listItemElement.removeStyleClass("hovered");
    340             }
    341         }
    342     },
    343 
    344     createTooltipForImageNode: function(node, callback)
    345     {
    346         function createTooltipThenCallback(properties)
    347         {
    348             if (!properties) {
    349                 callback();
    350                 return;
    351             }
    352 
    353             var tooltipText = null;
    354             if (properties.offsetHeight === properties.naturalHeight && properties.offsetWidth === properties.naturalWidth)
    355                 tooltipText = WebInspector.UIString("%d  %d pixels", properties.offsetWidth, properties.offsetHeight);
    356             else
    357                 tooltipText = WebInspector.UIString("%d  %d pixels (Natural: %d  %d pixels)", properties.offsetWidth, properties.offsetHeight, properties.naturalWidth, properties.naturalHeight);
    358             callback(tooltipText);
    359         }
    360         var objectProxy = new WebInspector.ObjectProxy(node.injectedScriptId, node.id);
    361         WebInspector.ObjectProxy.getPropertiesAsync(objectProxy, ["naturalHeight", "naturalWidth", "offsetHeight", "offsetWidth"], createTooltipThenCallback);
    362     },
    363 
    364     updateSelection: function()
    365     {
    366         var listItemElement = this.listItemElement;
    367         if (!listItemElement)
    368             return;
    369 
    370         if (document.body.offsetWidth <= 0) {
    371             // The stylesheet hasn't loaded yet or the window is closed,
    372             // so we can't calculate what is need. Return early.
    373             return;
    374         }
    375 
    376         if (!this.selectionElement) {
    377             this.selectionElement = document.createElement("div");
    378             this.selectionElement.className = "selection selected";
    379             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
    380         }
    381 
    382         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
    383     },
    384 
    385     onattach: function()
    386     {
    387         this.listItemElement.addEventListener("mousedown", this.onmousedown.bind(this), false);
    388 
    389         if (this._highlighted)
    390             this.listItemElement.addStyleClass("highlighted");
    391 
    392         if (this._hovered) {
    393             this.updateSelection();
    394             this.listItemElement.addStyleClass("hovered");
    395         }
    396 
    397         this.updateTitle();
    398 
    399         this._preventFollowingLinksOnDoubleClick();
    400     },
    401 
    402     _preventFollowingLinksOnDoubleClick: function()
    403     {
    404         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");
    405         if (!links)
    406             return;
    407 
    408         for (var i = 0; i < links.length; ++i)
    409             links[i].preventFollowOnDoubleClick = true;
    410     },
    411 
    412     onpopulate: function()
    413     {
    414         if (this.children.length || this._showInlineText(this.representedObject))
    415             return;
    416 
    417         this.updateChildren();
    418     },
    419 
    420     updateChildren: function(fullRefresh)
    421     {
    422         WebInspector.domAgent.getChildNodesAsync(this.representedObject, this._updateChildren.bind(this, fullRefresh));
    423     },
    424 
    425     _updateChildren: function(fullRefresh)
    426     {
    427         if (fullRefresh) {
    428             var selectedTreeElement = this.treeOutline.selectedTreeElement;
    429             if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
    430                 this.select();
    431             this.removeChildren();
    432         }
    433 
    434         var treeElement = this;
    435         var treeChildIndex = 0;
    436 
    437         function updateChildrenOfNode(node)
    438         {
    439             var treeOutline = treeElement.treeOutline;
    440             var child = node.firstChild;
    441             while (child) {
    442                 var currentTreeElement = treeElement.children[treeChildIndex];
    443                 if (!currentTreeElement || currentTreeElement.representedObject !== child) {
    444                     // Find any existing element that is later in the children list.
    445                     var existingTreeElement = null;
    446                     for (var i = (treeChildIndex + 1); i < treeElement.children.length; ++i) {
    447                         if (treeElement.children[i].representedObject === child) {
    448                             existingTreeElement = treeElement.children[i];
    449                             break;
    450                         }
    451                     }
    452 
    453                     if (existingTreeElement && existingTreeElement.parent === treeElement) {
    454                         // If an existing element was found and it has the same parent, just move it.
    455                         var wasSelected = existingTreeElement.selected;
    456                         treeElement.removeChild(existingTreeElement);
    457                         treeElement.insertChild(existingTreeElement, treeChildIndex);
    458                         if (wasSelected)
    459                             existingTreeElement.select();
    460                     } else {
    461                         // No existing element found, insert a new element.
    462                         var newElement = new WebInspector.ElementsTreeElement(child);
    463                         newElement.selectable = treeOutline.selectEnabled;
    464                         treeElement.insertChild(newElement, treeChildIndex);
    465                     }
    466                 }
    467 
    468                 child = child.nextSibling;
    469                 ++treeChildIndex;
    470             }
    471         }
    472 
    473         // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
    474         for (var i = (this.children.length - 1); i >= 0; --i) {
    475             if ("elementCloseTag" in this.children[i])
    476                 continue;
    477 
    478             var currentChild = this.children[i];
    479             var currentNode = currentChild.representedObject;
    480             var currentParentNode = currentNode.parentNode;
    481 
    482             if (currentParentNode === this.representedObject)
    483                 continue;
    484 
    485             var selectedTreeElement = this.treeOutline.selectedTreeElement;
    486             if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
    487                 this.select();
    488 
    489             this.removeChildAtIndex(i);
    490         }
    491 
    492         updateChildrenOfNode(this.representedObject);
    493 
    494         var lastChild = this.children[this.children.length - 1];
    495         if (this.representedObject.nodeType == Node.ELEMENT_NODE && (!lastChild || !lastChild.elementCloseTag)) {
    496             var title = "<span class=\"webkit-html-tag close\">&lt;/" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "&gt;</span>";
    497             var item = new TreeElement(title, null, false);
    498             item.selectable = false;
    499             item.elementCloseTag = true;
    500             this.appendChild(item);
    501         }
    502     },
    503 
    504     onexpand: function()
    505     {
    506         this.treeOutline.updateSelection();
    507     },
    508 
    509     oncollapse: function()
    510     {
    511         this.treeOutline.updateSelection();
    512     },
    513 
    514     onreveal: function()
    515     {
    516         if (this.listItemElement)
    517             this.listItemElement.scrollIntoViewIfNeeded(false);
    518     },
    519 
    520     onselect: function()
    521     {
    522         this.treeOutline.focusedDOMNode = this.representedObject;
    523         this.updateSelection();
    524     },
    525 
    526     onmousedown: function(event)
    527     {
    528         if (this._editing)
    529             return;
    530 
    531         if (this.isEventWithinDisclosureTriangle(event))
    532             return;
    533 
    534         if (this.treeOutline.showInElementsPanelEnabled) {
    535             WebInspector.showElementsPanel();
    536             WebInspector.panels.elements.focusedDOMNode = this.representedObject;
    537         }
    538 
    539         // Prevent selecting the nearest word on double click.
    540         if (event.detail >= 2)
    541             event.preventDefault();
    542     },
    543 
    544     ondblclick: function(event)
    545     {
    546         if (this._editing)
    547             return;
    548 
    549         if (this._startEditingFromEvent(event))
    550             return;
    551 
    552         if (this.hasChildren && !this.expanded)
    553             this.expand();
    554     },
    555 
    556     _insertInLastAttributePosition: function(tag, node)
    557     {
    558         if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
    559             tag.insertBefore(node, tag.lastChild);
    560         else {
    561             var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
    562             tag.textContent = '';
    563             tag.appendChild(document.createTextNode('<'+nodeName));
    564             tag.appendChild(node);
    565             tag.appendChild(document.createTextNode('>'));
    566         }
    567 
    568         this.updateSelection();
    569     },
    570 
    571     _startEditingFromEvent: function(event)
    572     {
    573         if (this.treeOutline.focusedDOMNode != this.representedObject)
    574             return;
    575 
    576         if (this.representedObject.nodeType != Node.ELEMENT_NODE && this.representedObject.nodeType != Node.TEXT_NODE)
    577             return false;
    578 
    579         var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
    580         if (textNode)
    581             return this._startEditingTextNode(textNode);
    582 
    583         var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
    584         if (attribute)
    585             return this._startEditingAttribute(attribute, event.target);
    586 
    587         var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
    588         if (newAttribute)
    589             return this._addNewAttribute();
    590 
    591         return false;
    592     },
    593 
    594     _populateTagContextMenu: function(contextMenu, event)
    595     {
    596         var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
    597         var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
    598 
    599         // Add attribute-related actions.
    600         contextMenu.appendItem(WebInspector.UIString("Add Attribute"), this._addNewAttribute.bind(this));
    601         if (attribute && !newAttribute)
    602             contextMenu.appendItem(WebInspector.UIString("Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
    603         contextMenu.appendSeparator();
    604 
    605         // Add node-related actions.
    606         contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
    607         contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
    608         contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this));
    609     },
    610 
    611     _populateTextContextMenu: function(contextMenu, textNode)
    612     {
    613         contextMenu.appendItem(WebInspector.UIString("Edit Text"), this._startEditingTextNode.bind(this, textNode));
    614     },
    615 
    616     _startEditing: function()
    617     {
    618         if (this.treeOutline.focusedDOMNode !== this.representedObject)
    619             return;
    620 
    621         var listItem = this._listItemNode;
    622 
    623         if (this._canAddAttributes) {
    624             var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
    625             if (attribute)
    626                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]);
    627 
    628             return this._addNewAttribute();
    629         }
    630 
    631         if (this.representedObject.nodeType === Node.TEXT_NODE) {
    632             var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0];
    633             if (textNode)
    634                 return this._startEditingTextNode(textNode);
    635             return;
    636         }
    637     },
    638 
    639     _addNewAttribute: function()
    640     {
    641         var attr = document.createElement("span");
    642         attr.className = "webkit-html-attribute";
    643         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
    644         attr.style.marginRight = "2px"; // overrides the .editing margin rule
    645         var name = document.createElement("span");
    646         name.className = "webkit-html-attribute-name new-attribute";
    647         name.textContent = " ";
    648         var value = document.createElement("span");
    649         value.className = "webkit-html-attribute-value";
    650         attr.appendChild(name);
    651         attr.appendChild(value);
    652 
    653         var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0];
    654         this._insertInLastAttributePosition(tag, attr);
    655         return this._startEditingAttribute(attr, attr);
    656     },
    657 
    658     _triggerEditAttribute: function(attributeName)
    659     {
    660         var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name");
    661         for (var i = 0, len = attributeElements.length; i < len; ++i) {
    662             if (attributeElements[i].textContent === attributeName) {
    663                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
    664                     if (elem.nodeType !== Node.ELEMENT_NODE)
    665                         continue;
    666 
    667                     if (elem.hasStyleClass("webkit-html-attribute-value"))
    668                         return this._startEditingAttribute(attributeElements[i].parentNode, elem);
    669                 }
    670             }
    671         }
    672     },
    673 
    674     _startEditingAttribute: function(attribute, elementForSelection)
    675     {
    676         if (WebInspector.isBeingEdited(attribute))
    677             return true;
    678 
    679         var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
    680         if (!attributeNameElement)
    681             return false;
    682 
    683         var attributeName = attributeNameElement.innerText;
    684 
    685         function removeZeroWidthSpaceRecursive(node)
    686         {
    687             if (node.nodeType === Node.TEXT_NODE) {
    688                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
    689                 return;
    690             }
    691 
    692             if (node.nodeType !== Node.ELEMENT_NODE)
    693                 return;
    694 
    695             for (var child = node.firstChild; child; child = child.nextSibling)
    696                 removeZeroWidthSpaceRecursive(child);
    697         }
    698 
    699         // Remove zero-width spaces that were added by nodeTitleInfo.
    700         removeZeroWidthSpaceRecursive(attribute);
    701 
    702         this._editing = true;
    703 
    704         WebInspector.startEditing(attribute, this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
    705         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
    706 
    707         return true;
    708     },
    709 
    710     _startEditingTextNode: function(textNode)
    711     {
    712         if (WebInspector.isBeingEdited(textNode))
    713             return true;
    714 
    715         this._editing = true;
    716 
    717         WebInspector.startEditing(textNode, this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
    718         window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
    719 
    720         return true;
    721     },
    722 
    723     _startEditingAsHTML: function(commitCallback, initialValue)
    724     {
    725         if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement))
    726             return true;
    727 
    728         this._editing = true;
    729 
    730         this._htmlEditElement = document.createElement("div");
    731         this._htmlEditElement.className = "source-code elements-tree-editor";
    732         this._htmlEditElement.textContent = initialValue;
    733 
    734         // Hide header items.
    735         var child = this.listItemElement.firstChild;
    736         while (child) {
    737             child.style.display = "none";
    738             child = child.nextSibling;
    739         }
    740         // Hide children item.
    741         if (this._childrenListNode)
    742             this._childrenListNode.style.display = "none";
    743         // Append editor.
    744         this.listItemElement.appendChild(this._htmlEditElement);
    745 
    746         this.updateSelection();
    747 
    748         function commit()
    749         {
    750             commitCallback(this._htmlEditElement.textContent);
    751             dispose.call(this);
    752         }
    753 
    754         function dispose()
    755         {
    756             delete this._editing;
    757 
    758             // Remove editor.
    759             this.listItemElement.removeChild(this._htmlEditElement);
    760             delete this._htmlEditElement;
    761             // Unhide children item.
    762             if (this._childrenListNode)
    763                 this._childrenListNode.style.removeProperty("display");
    764             // Unhide header items.
    765             var child = this.listItemElement.firstChild;
    766             while (child) {
    767                 child.style.removeProperty("display");
    768                 child = child.nextSibling;
    769             }
    770 
    771             this.updateSelection();
    772         }
    773 
    774         WebInspector.startEditing(this._htmlEditElement, commit.bind(this), dispose.bind(this), null, true);
    775     },
    776 
    777     _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
    778     {
    779         delete this._editing;
    780 
    781         // Before we do anything, determine where we should move
    782         // next based on the current element's settings
    783         var moveToAttribute;
    784         var newAttribute;
    785         if (moveDirection) {
    786             var found = false;
    787             var attributes = this.representedObject.attributes;
    788             for (var i = 0, len = attributes.length; i < len; ++i) {
    789                 if (attributes[i].name === attributeName) {
    790                     found = true;
    791                     if (moveDirection === "backward" && i > 0)
    792                         moveToAttribute = attributes[i - 1].name;
    793                     else if (moveDirection === "forward" && i < attributes.length - 1)
    794                         moveToAttribute = attributes[i + 1].name;
    795                     else if (moveDirection === "forward" && i === attributes.length - 1)
    796                         newAttribute = true;
    797                 }
    798             }
    799 
    800             if (!found && moveDirection === "backward" && attributes.length > 0)
    801                 moveToAttribute = attributes[attributes.length - 1].name;
    802             else if (!found && moveDirection === "forward" && !/^\s*$/.test(newText))
    803                 newAttribute = true;
    804         }
    805 
    806         function moveToNextAttributeIfNeeded() {
    807             if (moveToAttribute)
    808                 this._triggerEditAttribute(moveToAttribute);
    809             else if (newAttribute)
    810                 this._addNewAttribute(this.listItemElement);
    811         }
    812 
    813         var parseContainerElement = document.createElement("span");
    814         parseContainerElement.innerHTML = "<span " + newText + "></span>";
    815         var parseElement = parseContainerElement.firstChild;
    816 
    817         if (!parseElement) {
    818             this._editingCancelled(element, attributeName);
    819             moveToNextAttributeIfNeeded.call(this);
    820             return;
    821         }
    822 
    823         if (!parseElement.hasAttributes()) {
    824             this.representedObject.removeAttribute(attributeName);
    825             moveToNextAttributeIfNeeded.call(this);
    826             return;
    827         }
    828 
    829         var foundOriginalAttribute = false;
    830         for (var i = 0; i < parseElement.attributes.length; ++i) {
    831             var attr = parseElement.attributes[i];
    832             foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName;
    833             try {
    834                 this.representedObject.setAttribute(attr.name, attr.value);
    835             } catch(e) {} // ignore invalid attribute (innerHTML doesn't throw errors, but this can)
    836         }
    837 
    838         if (!foundOriginalAttribute)
    839             this.representedObject.removeAttribute(attributeName);
    840 
    841         this.treeOutline.focusedNodeChanged(true);
    842 
    843         moveToNextAttributeIfNeeded.call(this);
    844     },
    845 
    846     _textNodeEditingCommitted: function(element, newText)
    847     {
    848         delete this._editing;
    849 
    850         var textNode;
    851         if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
    852             // We only show text nodes inline in elements if the element only
    853             // has a single child, and that child is a text node.
    854             textNode = this.representedObject.firstChild;
    855         } else if (this.representedObject.nodeType == Node.TEXT_NODE)
    856             textNode = this.representedObject;
    857 
    858         textNode.nodeValue = newText;
    859 
    860         // Need to restore attributes / node structure.
    861         this.updateTitle();
    862     },
    863 
    864     _editingCancelled: function(element, context)
    865     {
    866         delete this._editing;
    867 
    868         // Need to restore attributes structure.
    869         this.updateTitle();
    870     },
    871 
    872     updateTitle: function()
    873     {
    874         // If we are editing, return early to prevent canceling the edit.
    875         // After editing is committed updateTitle will be called.
    876         if (this._editing)
    877             return;
    878 
    879         var self = this;
    880         function callback(tooltipText)
    881         {
    882             var title = self._nodeTitleInfo(self.representedObject, self.hasChildren, WebInspector.linkifyURL, tooltipText).title;
    883             self.title = "<span class=\"highlight\">" + title + "</span>";
    884             delete self.selectionElement;
    885             self.updateSelection();
    886             self._preventFollowingLinksOnDoubleClick();
    887         };
    888 
    889         // TODO: Replace with InjectedScriptAccess.getBasicProperties(obj, [names]).
    890         if (this.representedObject.nodeName.toLowerCase() !== "img")
    891             callback();
    892         else
    893             this.createTooltipForImageNode(this.representedObject, callback);
    894     },
    895 
    896     _rewriteAttrHref: function(node, hrefValue)
    897     {
    898         if (!hrefValue || hrefValue.indexOf("://") > 0)
    899             return hrefValue;
    900 
    901         for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
    902             if (frameOwnerCandidate.documentURL) {
    903                 var result = WebInspector.completeURL(frameOwnerCandidate.documentURL, hrefValue);
    904                 if (result)
    905                     return result;
    906                 break;
    907             }
    908         }
    909 
    910         // documentURL not found or has bad value
    911         for (var url in WebInspector.resourceURLMap) {
    912             var match = url.match(WebInspector.URLRegExp);
    913             if (match && match[4] === hrefValue)
    914                 return url;
    915         }
    916         return hrefValue;
    917     },
    918 
    919     _nodeTitleInfo: function(node, hasChildren, linkify, tooltipText)
    920     {
    921         var info = {title: "", hasChildren: hasChildren};
    922 
    923         switch (node.nodeType) {
    924             case Node.DOCUMENT_NODE:
    925                 info.title = "Document";
    926                 break;
    927 
    928             case Node.DOCUMENT_FRAGMENT_NODE:
    929                 info.title = "Document Fragment";
    930                 break;
    931 
    932             case Node.ELEMENT_NODE:
    933                 info.title = "<span class=\"webkit-html-tag\">&lt;" + node.nodeName.toLowerCase().escapeHTML();
    934 
    935                 if (node.hasAttributes()) {
    936                     for (var i = 0; i < node.attributes.length; ++i) {
    937                         var attr = node.attributes[i];
    938                         info.title += " <span class=\"webkit-html-attribute\"><span class=\"webkit-html-attribute-name\">" + attr.name.escapeHTML() + "</span>=&#8203;\"";
    939 
    940                         var value = attr.value;
    941                         if (linkify && (attr.name === "src" || attr.name === "href")) {
    942                             var value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B");
    943                             info.title += linkify(this._rewriteAttrHref(node, attr.value), value, "webkit-html-attribute-value", node.nodeName.toLowerCase() == "a", tooltipText);
    944                         } else {
    945                             var value = value.escapeHTML();
    946                             value = value.replace(/([\/;:\)\]\}])/g, "$1&#8203;");
    947                             info.title += "<span class=\"webkit-html-attribute-value\">" + value + "</span>";
    948                         }
    949                         info.title += "\"</span>";
    950                     }
    951                 }
    952                 info.title += "&gt;</span>&#8203;";
    953 
    954                 // If this element only has a single child that is a text node,
    955                 // just show that text and the closing tag inline rather than
    956                 // create a subtree for them
    957 
    958                 var textChild = onlyTextChild.call(node);
    959                 var showInlineText = textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength;
    960 
    961                 if (showInlineText) {
    962                     info.title += "<span class=\"webkit-html-text-node\">" + textChild.nodeValue.escapeHTML() + "</span>&#8203;<span class=\"webkit-html-tag\">&lt;/" + node.nodeName.toLowerCase().escapeHTML() + "&gt;</span>";
    963                     info.hasChildren = false;
    964                 }
    965                 break;
    966 
    967             case Node.TEXT_NODE:
    968                 if (isNodeWhitespace.call(node))
    969                     info.title = "(whitespace)";
    970                 else {
    971                     if (node.parentNode && node.parentNode.nodeName.toLowerCase() == "script") {
    972                         var newNode = document.createElement("span");
    973                         newNode.textContent = node.textContent;
    974 
    975                         var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript");
    976                         javascriptSyntaxHighlighter.syntaxHighlightNode(newNode);
    977 
    978                         info.title = "<span class=\"webkit-html-text-node webkit-html-js-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>";
    979                     } else if (node.parentNode && node.parentNode.nodeName.toLowerCase() == "style") {
    980                         var newNode = document.createElement("span");
    981                         newNode.textContent = node.textContent;
    982 
    983                         var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css");
    984                         cssSyntaxHighlighter.syntaxHighlightNode(newNode);
    985 
    986                         info.title = "<span class=\"webkit-html-text-node webkit-html-css-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>";
    987                     } else {
    988                         info.title = "\"<span class=\"webkit-html-text-node\">" + node.nodeValue.escapeHTML() + "</span>\"";
    989                     }
    990                 }
    991                 break;
    992 
    993             case Node.COMMENT_NODE:
    994                 info.title = "<span class=\"webkit-html-comment\">&lt;!--" + node.nodeValue.escapeHTML() + "--&gt;</span>";
    995                 break;
    996 
    997             case Node.DOCUMENT_TYPE_NODE:
    998                 info.title = "<span class=\"webkit-html-doctype\">&lt;!DOCTYPE " + node.nodeName;
    999                 if (node.publicId) {
   1000                     info.title += " PUBLIC \"" + node.publicId + "\"";
   1001                     if (node.systemId)
   1002                         info.title += " \"" + node.systemId + "\"";
   1003                 } else if (node.systemId)
   1004                     info.title += " SYSTEM \"" + node.systemId + "\"";
   1005                 if (node.internalSubset)
   1006                     info.title += " [" + node.internalSubset + "]";
   1007                 info.title += "&gt;</span>";
   1008                 break;
   1009             default:
   1010                 info.title = node.nodeName.toLowerCase().collapseWhitespace().escapeHTML();
   1011         }
   1012 
   1013         return info;
   1014     },
   1015 
   1016     _showInlineText: function(node)
   1017     {
   1018         if (node.nodeType === Node.ELEMENT_NODE) {
   1019             var textChild = onlyTextChild.call(node);
   1020             if (textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength)
   1021                 return true;
   1022         }
   1023         return false;
   1024     },
   1025 
   1026     remove: function()
   1027     {
   1028         var parentElement = this.parent;
   1029         if (!parentElement)
   1030             return;
   1031 
   1032         var self = this;
   1033         function removeNodeCallback(removedNodeId)
   1034         {
   1035             // -1 is an error code, which means removing the node from the DOM failed,
   1036             // so we shouldn't remove it from the tree.
   1037             if (removedNodeId === -1)
   1038                 return;
   1039 
   1040             parentElement.removeChild(self);
   1041         }
   1042 
   1043         var callId = WebInspector.Callback.wrap(removeNodeCallback);
   1044         InspectorBackend.removeNode(callId, this.representedObject.id);
   1045     },
   1046 
   1047     _editAsHTML: function()
   1048     {
   1049         var treeOutline = this.treeOutline;
   1050         var node = this.representedObject;
   1051         var wasExpanded = this.expanded;
   1052 
   1053         function selectNode(nodeId)
   1054         {
   1055             if (!nodeId)
   1056                 return;
   1057 
   1058             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
   1059             WebInspector.panels.elements.updateModifiedNodes();
   1060 
   1061             WebInspector.updateFocusedNode(nodeId);
   1062             if (wasExpanded) {
   1063                 var newTreeItem = treeOutline.findTreeElement(WebInspector.domAgent.nodeForId(nodeId));
   1064                 if (newTreeItem)
   1065                     newTreeItem.expand();
   1066             }
   1067         }
   1068 
   1069         function commitChange(value)
   1070         {
   1071             InjectedScriptAccess.get(node.injectedScriptId).setOuterHTML(node.id, value, wasExpanded, selectNode.bind(this));
   1072         }
   1073 
   1074         InjectedScriptAccess.get(node.injectedScriptId).getNodePropertyValue(node.id, "outerHTML", this._startEditingAsHTML.bind(this, commitChange));
   1075     },
   1076 
   1077     _copyHTML: function()
   1078     {
   1079         InspectorBackend.copyNode(this.representedObject.id);
   1080     }
   1081 }
   1082 
   1083 WebInspector.ElementsTreeElement.prototype.__proto__ = TreeElement.prototype;
   1084 
   1085 WebInspector.didRemoveNode = WebInspector.Callback.processCallback;
   1086