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 
     46 WebInspector.ElementsTreeOutline.prototype = {
     47     get rootDOMNode()
     48     {
     49         return this._rootDOMNode;
     50     },
     51 
     52     set rootDOMNode(x)
     53     {
     54         if (this._rootDOMNode === x)
     55             return;
     56 
     57         this._rootDOMNode = x;
     58 
     59         this._isXMLMimeType = !!(WebInspector.mainResource && WebInspector.mainResource.mimeType && WebInspector.mainResource.mimeType.match(/x(?:ht)?ml/i));
     60 
     61         this.update();
     62     },
     63 
     64     get isXMLMimeType()
     65     {
     66         return this._isXMLMimeType;
     67     },
     68 
     69     nodeNameToCorrectCase: function(nodeName)
     70     {
     71         return this.isXMLMimeType ? nodeName : nodeName.toLowerCase();
     72     },
     73 
     74     get focusedDOMNode()
     75     {
     76         return this._focusedDOMNode;
     77     },
     78 
     79     set focusedDOMNode(x)
     80     {
     81         if (this._focusedDOMNode === x) {
     82             this.revealAndSelectNode(x);
     83             return;
     84         }
     85 
     86         this._focusedDOMNode = x;
     87 
     88         this.revealAndSelectNode(x);
     89 
     90         // The revealAndSelectNode() method might find a different element if there is inlined text,
     91         // and the select() call would change the focusedDOMNode and reenter this setter. So to
     92         // avoid calling focusedNodeChanged() twice, first check if _focusedDOMNode is the same
     93         // node as the one passed in.
     94         if (this._focusedDOMNode === x)
     95             this.focusedNodeChanged();
     96     },
     97 
     98     get editing()
     99     {
    100         return this._editing;
    101     },
    102 
    103     update: function()
    104     {
    105         var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
    106 
    107         this.removeChildren();
    108 
    109         if (!this.rootDOMNode)
    110             return;
    111 
    112         var treeElement;
    113         if (this.includeRootDOMNode) {
    114             treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode);
    115             treeElement.selectable = this.selectEnabled;
    116             this.appendChild(treeElement);
    117         } else {
    118             // FIXME: this could use findTreeElement to reuse a tree element if it already exists
    119             var node = this.rootDOMNode.firstChild;
    120             while (node) {
    121                 treeElement = new WebInspector.ElementsTreeElement(node);
    122                 treeElement.selectable = this.selectEnabled;
    123                 this.appendChild(treeElement);
    124                 node = node.nextSibling;
    125             }
    126         }
    127 
    128         if (selectedNode)
    129             this.revealAndSelectNode(selectedNode);
    130     },
    131 
    132     updateSelection: function()
    133     {
    134         if (!this.selectedTreeElement)
    135             return;
    136         var element = this.treeOutline.selectedTreeElement;
    137         element.updateSelection();
    138     },
    139 
    140     focusedNodeChanged: function(forceUpdate) {},
    141 
    142     findTreeElement: function(node)
    143     {
    144         var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode);
    145         if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
    146             // The text node might have been inlined if it was short, so try to find the parent element.
    147             treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode);
    148         }
    149 
    150         return treeElement;
    151     },
    152 
    153     createTreeElementFor: function(node)
    154     {
    155         var treeElement = this.findTreeElement(node);
    156         if (treeElement)
    157             return treeElement;
    158         if (!node.parentNode)
    159             return null;
    160 
    161         var treeElement = this.createTreeElementFor(node.parentNode);
    162         if (treeElement && treeElement.showChild(node.index))
    163             return treeElement.children[node.index];
    164 
    165         return null;
    166     },
    167 
    168     set suppressRevealAndSelect(x)
    169     {
    170         if (this._suppressRevealAndSelect === x)
    171             return;
    172         this._suppressRevealAndSelect = x;
    173     },
    174 
    175     revealAndSelectNode: function(node)
    176     {
    177         if (!node || this._suppressRevealAndSelect)
    178             return;
    179 
    180         var treeElement = this.createTreeElementFor(node);
    181         if (!treeElement)
    182             return;
    183 
    184         treeElement.reveal();
    185         treeElement.select();
    186     },
    187 
    188     _treeElementFromEvent: function(event)
    189     {
    190         var scrollContainer = this.element.parentElement;
    191 
    192         // We choose this X coordinate based on the knowledge that our list
    193         // items extend at least to the right edge of the outer <ol> container.
    194         // In the no-word-wrap mode the outer <ol> may be wider than the tree container
    195         // (and partially hidden), in which case we are left to use only its right boundary.
    196         var x = scrollContainer.totalOffsetLeft + scrollContainer.offsetWidth - 36;
    197 
    198         var y = event.pageY;
    199 
    200         // Our list items have 1-pixel cracks between them vertically. We avoid
    201         // the cracks by checking slightly above and slightly below the mouse
    202         // and seeing if we hit the same element each time.
    203         var elementUnderMouse = this.treeElementFromPoint(x, y);
    204         var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
    205         var element;
    206         if (elementUnderMouse === elementAboveMouse)
    207             element = elementUnderMouse;
    208         else
    209             element = this.treeElementFromPoint(x, y + 2);
    210 
    211         return element;
    212     },
    213 
    214     _onmousedown: function(event)
    215     {
    216         var element = this._treeElementFromEvent(event);
    217 
    218         if (!element || element.isEventWithinDisclosureTriangle(event))
    219             return;
    220 
    221         element.select();
    222     },
    223 
    224     _onmousemove: function(event)
    225     {
    226         var element = this._treeElementFromEvent(event);
    227         if (element && this._previousHoveredElement === element)
    228             return;
    229 
    230         if (this._previousHoveredElement) {
    231             this._previousHoveredElement.hovered = false;
    232             delete this._previousHoveredElement;
    233         }
    234 
    235         if (element) {
    236             element.hovered = true;
    237             this._previousHoveredElement = element;
    238 
    239             // Lazily compute tag-specific tooltips.
    240             if (element.representedObject && !element.tooltip)
    241                 element._createTooltipForNode();
    242         }
    243 
    244         WebInspector.highlightDOMNode(element ? element.representedObject.id : 0);
    245     },
    246 
    247     _onmouseout: function(event)
    248     {
    249         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
    250         if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
    251             return;
    252 
    253         if (this._previousHoveredElement) {
    254             this._previousHoveredElement.hovered = false;
    255             delete this._previousHoveredElement;
    256         }
    257 
    258         WebInspector.highlightDOMNode(0);
    259     },
    260 
    261     populateContextMenu: function(contextMenu, event)
    262     {
    263         var listItem = event.target.enclosingNodeOrSelfWithNodeName("LI");
    264         if (!listItem || !listItem.treeElement)
    265             return false;
    266 
    267         var populated;
    268         if (this.showInElementsPanelEnabled) {
    269             function focusElement()
    270             {
    271                 WebInspector.panels.elements.switchToAndFocus(listItem.treeElement.representedObject);
    272             }
    273             contextMenu.appendItem(WebInspector.UIString("Reveal in Elements Panel"), focusElement.bind(this));
    274             populated = true;
    275         } else {
    276             var href = event.target.enclosingNodeOrSelfWithClass("webkit-html-resource-link") || event.target.enclosingNodeOrSelfWithClass("webkit-html-external-link");
    277             var tag = event.target.enclosingNodeOrSelfWithClass("webkit-html-tag");
    278             var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
    279             if (href)
    280                 populated = WebInspector.panels.elements.populateHrefContextMenu(contextMenu, event, href);
    281             if (tag && listItem.treeElement._populateTagContextMenu) {
    282                 if (populated)
    283                     contextMenu.appendSeparator();
    284                 listItem.treeElement._populateTagContextMenu(contextMenu, event);
    285                 populated = true;
    286             } else if (textNode && listItem.treeElement._populateTextContextMenu) {
    287                 if (populated)
    288                     contextMenu.appendSeparator();
    289                 listItem.treeElement._populateTextContextMenu(contextMenu, textNode);
    290                 populated = true;
    291             }
    292         }
    293 
    294         return populated;
    295     }
    296 }
    297 
    298 WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype;
    299 
    300 WebInspector.ElementsTreeElement = function(node, elementCloseTag)
    301 {
    302     this._elementCloseTag = elementCloseTag;
    303     var hasChildrenOverride = !elementCloseTag && node.hasChildNodes() && !this._showInlineText(node);
    304 
    305     // The title will be updated in onattach.
    306     TreeElement.call(this, "", node, hasChildrenOverride);
    307 
    308     if (this.representedObject.nodeType() == Node.ELEMENT_NODE && !elementCloseTag)
    309         this._canAddAttributes = true;
    310     this._searchQuery = null;
    311     this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit;
    312 }
    313 
    314 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
    315 
    316 // A union of HTML4 and HTML5-Draft elements that explicitly
    317 // or implicitly (for HTML5) forbid the closing tag.
    318 // FIXME: Revise once HTML5 Final is published.
    319 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [
    320     "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
    321     "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source"
    322 ].keySet();
    323 
    324 // These tags we do not allow editing their tag name.
    325 WebInspector.ElementsTreeElement.EditTagBlacklist = [
    326     "html", "head", "body"
    327 ].keySet();
    328 
    329 WebInspector.ElementsTreeElement.prototype = {
    330     highlightSearchResults: function(searchQuery)
    331     {
    332         if (this._searchQuery === searchQuery)
    333             return;
    334 
    335         if (searchQuery)
    336             delete this._searchHighlightedHTML; // A new search query (not clear-the-current-highlighting).
    337 
    338         this._searchQuery = searchQuery;
    339         this.updateTitle(true);
    340     },
    341 
    342     get hovered()
    343     {
    344         return this._hovered;
    345     },
    346 
    347     set hovered(x)
    348     {
    349         if (this._hovered === x)
    350             return;
    351 
    352         this._hovered = x;
    353 
    354         if (this.listItemElement) {
    355             if (x) {
    356                 this.updateSelection();
    357                 this.listItemElement.addStyleClass("hovered");
    358             } else {
    359                 this.listItemElement.removeStyleClass("hovered");
    360             }
    361         }
    362     },
    363 
    364     get expandedChildrenLimit()
    365     {
    366         return this._expandedChildrenLimit;
    367     },
    368 
    369     set expandedChildrenLimit(x)
    370     {
    371         if (this._expandedChildrenLimit === x)
    372             return;
    373 
    374         this._expandedChildrenLimit = x;
    375         if (this.treeOutline && !this._updateChildrenInProgress)
    376             this._updateChildren(true);
    377     },
    378 
    379     get expandedChildCount()
    380     {
    381         var count = this.children.length;
    382         if (count && this.children[count - 1]._elementCloseTag)
    383             count--;
    384         if (count && this.children[count - 1].expandAllButton)
    385             count--;
    386         return count;
    387     },
    388 
    389     showChild: function(index)
    390     {
    391         if (this._elementCloseTag)
    392             return;
    393 
    394         if (index >= this.expandedChildrenLimit) {
    395             this._expandedChildrenLimit = index + 1;
    396             this._updateChildren(true);
    397         }
    398 
    399         // Whether index-th child is visible in the children tree
    400         return this.expandedChildCount > index;
    401     },
    402 
    403     _createTooltipForNode: function()
    404     {
    405         var node = this.representedObject;
    406         if (!node.nodeName() || node.nodeName().toLowerCase() !== "img")
    407             return;
    408 
    409         function setTooltip(error, result)
    410         {
    411             if (error || !result || result.type !== "string")
    412                 return;
    413 
    414             try {
    415                 var properties = JSON.parse(result.description);
    416                 var offsetWidth = properties[0];
    417                 var offsetHeight = properties[1];
    418                 var naturalWidth = properties[2];
    419                 var naturalHeight = properties[3];
    420                 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
    421                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels", offsetWidth, offsetHeight);
    422                 else
    423                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)", offsetWidth, offsetHeight, naturalWidth, naturalHeight);
    424             } catch (e) {
    425                 console.error(e);
    426             }
    427         }
    428 
    429         function resolvedNode(object)
    430         {
    431             if (!object)
    432                 return;
    433 
    434             object.evaluate("return '[' + this.offsetWidth + ',' + this.offsetHeight + ',' + this.naturalWidth + ',' + this.naturalHeight + ']'", setTooltip.bind(this));
    435             object.release();
    436         }
    437         WebInspector.RemoteObject.resolveNode(node, resolvedNode.bind(this));
    438     },
    439 
    440     updateSelection: function()
    441     {
    442         var listItemElement = this.listItemElement;
    443         if (!listItemElement)
    444             return;
    445 
    446         if (document.body.offsetWidth <= 0) {
    447             // The stylesheet hasn't loaded yet or the window is closed,
    448             // so we can't calculate what is need. Return early.
    449             return;
    450         }
    451 
    452         if (!this.selectionElement) {
    453             this.selectionElement = document.createElement("div");
    454             this.selectionElement.className = "selection selected";
    455             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
    456         }
    457 
    458         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
    459     },
    460 
    461     onattach: function()
    462     {
    463         if (this._hovered) {
    464             this.updateSelection();
    465             this.listItemElement.addStyleClass("hovered");
    466         }
    467 
    468         this.updateTitle();
    469 
    470         this._preventFollowingLinksOnDoubleClick();
    471     },
    472 
    473     _preventFollowingLinksOnDoubleClick: function()
    474     {
    475         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");
    476         if (!links)
    477             return;
    478 
    479         for (var i = 0; i < links.length; ++i)
    480             links[i].preventFollowOnDoubleClick = true;
    481     },
    482 
    483     onpopulate: function()
    484     {
    485         if (this.children.length || this._showInlineText(this.representedObject) || this._elementCloseTag)
    486             return;
    487 
    488         this.updateChildren();
    489     },
    490 
    491     updateChildren: function(fullRefresh)
    492     {
    493         if (this._elementCloseTag)
    494             return;
    495         this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh));
    496     },
    497 
    498     insertChildElement: function(child, index, closingTag)
    499     {
    500         var newElement = new WebInspector.ElementsTreeElement(child, closingTag);
    501         newElement.selectable = this.treeOutline.selectEnabled;
    502         this.insertChild(newElement, index);
    503         return newElement;
    504     },
    505 
    506     moveChild: function(child, targetIndex)
    507     {
    508         var wasSelected = child.selected;
    509         this.removeChild(child);
    510         this.insertChild(child, targetIndex);
    511         if (wasSelected)
    512             child.select();
    513     },
    514 
    515     _updateChildren: function(fullRefresh)
    516     {
    517         if (this._updateChildrenInProgress)
    518             return;
    519 
    520         this._updateChildrenInProgress = true;
    521         var focusedNode = this.treeOutline.focusedDOMNode;
    522         var originalScrollTop;
    523         if (fullRefresh) {
    524             var treeOutlineContainerElement = this.treeOutline.element.parentNode;
    525             originalScrollTop = treeOutlineContainerElement.scrollTop;
    526             var selectedTreeElement = this.treeOutline.selectedTreeElement;
    527             if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
    528                 this.select();
    529             this.removeChildren();
    530         }
    531 
    532         var treeElement = this;
    533         var treeChildIndex = 0;
    534         var elementToSelect;
    535 
    536         function updateChildrenOfNode(node)
    537         {
    538             var treeOutline = treeElement.treeOutline;
    539             var child = node.firstChild;
    540             while (child) {
    541                 var currentTreeElement = treeElement.children[treeChildIndex];
    542                 if (!currentTreeElement || currentTreeElement.representedObject !== child) {
    543                     // Find any existing element that is later in the children list.
    544                     var existingTreeElement = null;
    545                     for (var i = (treeChildIndex + 1), size = treeElement.expandedChildCount; i < size; ++i) {
    546                         if (treeElement.children[i].representedObject === child) {
    547                             existingTreeElement = treeElement.children[i];
    548                             break;
    549                         }
    550                     }
    551 
    552                     if (existingTreeElement && existingTreeElement.parent === treeElement) {
    553                         // If an existing element was found and it has the same parent, just move it.
    554                         treeElement.moveChild(existingTreeElement, treeChildIndex);
    555                     } else {
    556                         // No existing element found, insert a new element.
    557                         if (treeChildIndex < treeElement.expandedChildrenLimit) {
    558                             var newElement = treeElement.insertChildElement(child, treeChildIndex);
    559                             if (child === focusedNode)
    560                                 elementToSelect = newElement;
    561                             if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit)
    562                                 treeElement.expandedChildrenLimit++;
    563                         }
    564                     }
    565                 }
    566 
    567                 child = child.nextSibling;
    568                 ++treeChildIndex;
    569             }
    570         }
    571 
    572         // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
    573         for (var i = (this.children.length - 1); i >= 0; --i) {
    574             var currentChild = this.children[i];
    575             var currentNode = currentChild.representedObject;
    576             var currentParentNode = currentNode.parentNode;
    577 
    578             if (currentParentNode === this.representedObject)
    579                 continue;
    580 
    581             var selectedTreeElement = this.treeOutline.selectedTreeElement;
    582             if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
    583                 this.select();
    584 
    585             this.removeChildAtIndex(i);
    586         }
    587 
    588         updateChildrenOfNode(this.representedObject);
    589         this.adjustCollapsedRange(false);
    590 
    591         var lastChild = this.children[this.children.length - 1];
    592         if (this.representedObject.nodeType() == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
    593             this.insertChildElement(this.representedObject, this.children.length, true);
    594 
    595         // We want to restore the original selection and tree scroll position after a full refresh, if possible.
    596         if (fullRefresh && elementToSelect) {
    597             elementToSelect.select();
    598             if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
    599                 treeOutlineContainerElement.scrollTop = originalScrollTop;
    600         }
    601 
    602         delete this._updateChildrenInProgress;
    603     },
    604 
    605     adjustCollapsedRange: function()
    606     {
    607         // Ensure precondition: only the tree elements for node children are found in the tree
    608         // (not the Expand All button or the closing tag).
    609         if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
    610             this.removeChild(this.expandAllButtonElement.__treeElement);
    611 
    612         const node = this.representedObject;
    613         if (!node.children)
    614             return;
    615         const childNodeCount = node.children.length;
    616 
    617         // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
    618         for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
    619             this.insertChildElement(node.children[i], i);
    620 
    621         const expandedChildCount = this.expandedChildCount;
    622         if (childNodeCount > this.expandedChildCount) {
    623             var targetButtonIndex = expandedChildCount;
    624             if (!this.expandAllButtonElement) {
    625                 var item = new TreeElement(null, null, false);
    626                 item.titleHTML = "<button class=\"show-all-nodes\" value=\"\" />";
    627                 item.selectable = false;
    628                 item.expandAllButton = true;
    629                 this.insertChild(item, targetButtonIndex);
    630                 this.expandAllButtonElement = item.listItemElement.firstChild;
    631                 this.expandAllButtonElement.__treeElement = item;
    632                 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
    633             } else if (!this.expandAllButtonElement.__treeElement.parent)
    634                 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
    635             this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount);
    636         } else if (this.expandAllButtonElement)
    637             delete this.expandAllButtonElement;
    638     },
    639 
    640     handleLoadAllChildren: function()
    641     {
    642         this.expandedChildrenLimit = Math.max(this.representedObject._childNodeCount, this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit);
    643     },
    644 
    645     onexpand: function()
    646     {
    647         if (this._elementCloseTag)
    648             return;
    649 
    650         this.updateTitle();
    651         this.treeOutline.updateSelection();
    652     },
    653 
    654     oncollapse: function()
    655     {
    656         if (this._elementCloseTag)
    657             return;
    658 
    659         this.updateTitle();
    660         this.treeOutline.updateSelection();
    661     },
    662 
    663     onreveal: function()
    664     {
    665         if (this.listItemElement) {
    666             var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name");
    667             if (tagSpans.length)
    668                 tagSpans[0].scrollIntoViewIfNeeded(false);
    669             else
    670                 this.listItemElement.scrollIntoViewIfNeeded(false);
    671         }
    672     },
    673 
    674     onselect: function(treeElement, selectedByUser)
    675     {
    676         this.treeOutline.suppressRevealAndSelect = true;
    677         this.treeOutline.focusedDOMNode = this.representedObject;
    678         if (selectedByUser)
    679             WebInspector.highlightDOMNode(this.representedObject.id);
    680         this.updateSelection();
    681         this.treeOutline.suppressRevealAndSelect = false;
    682     },
    683 
    684     ondelete: function()
    685     {
    686         var startTagTreeElement = this.treeOutline.findTreeElement(this.representedObject);
    687         startTagTreeElement ? startTagTreeElement.remove() : this.remove();
    688         return true;
    689     },
    690 
    691     onenter: function()
    692     {
    693         // On Enter or Return start editing the first attribute
    694         // or create a new attribute on the selected element.
    695         if (this.treeOutline.editing)
    696             return false;
    697 
    698         this._startEditing();
    699 
    700         // prevent a newline from being immediately inserted
    701         return true;
    702     },
    703 
    704     selectOnMouseDown: function(event)
    705     {
    706         TreeElement.prototype.selectOnMouseDown.call(this, event);
    707 
    708         if (this._editing)
    709             return;
    710 
    711         if (this.treeOutline.showInElementsPanelEnabled) {
    712             WebInspector.showPanel("elements");
    713             WebInspector.panels.elements.focusedDOMNode = this.representedObject;
    714         }
    715 
    716         // Prevent selecting the nearest word on double click.
    717         if (event.detail >= 2)
    718             event.preventDefault();
    719     },
    720 
    721     ondblclick: function(event)
    722     {
    723         if (this._editing || this._elementCloseTag)
    724             return;
    725 
    726         if (this._startEditingTarget(event.target))
    727             return;
    728 
    729         if (this.hasChildren && !this.expanded)
    730             this.expand();
    731     },
    732 
    733     _insertInLastAttributePosition: function(tag, node)
    734     {
    735         if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
    736             tag.insertBefore(node, tag.lastChild);
    737         else {
    738             var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
    739             tag.textContent = '';
    740             tag.appendChild(document.createTextNode('<'+nodeName));
    741             tag.appendChild(node);
    742             tag.appendChild(document.createTextNode('>'));
    743         }
    744 
    745         this.updateSelection();
    746     },
    747 
    748     _startEditingTarget: function(eventTarget)
    749     {
    750         if (this.treeOutline.focusedDOMNode != this.representedObject)
    751             return;
    752 
    753         if (this.representedObject.nodeType() != Node.ELEMENT_NODE && this.representedObject.nodeType() != Node.TEXT_NODE)
    754             return false;
    755 
    756         var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node");
    757         if (textNode)
    758             return this._startEditingTextNode(textNode);
    759 
    760         var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute");
    761         if (attribute)
    762             return this._startEditingAttribute(attribute, eventTarget);
    763 
    764         var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
    765         if (tagName)
    766             return this._startEditingTagName(tagName);
    767 
    768         var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
    769         if (newAttribute)
    770             return this._addNewAttribute();
    771 
    772         return false;
    773     },
    774 
    775     _populateTagContextMenu: function(contextMenu, event)
    776     {
    777         var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
    778         var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
    779 
    780         // Add attribute-related actions.
    781         contextMenu.appendItem(WebInspector.UIString("Add Attribute"), this._addNewAttribute.bind(this));
    782         if (attribute && !newAttribute)
    783             contextMenu.appendItem(WebInspector.UIString("Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
    784         contextMenu.appendSeparator();
    785 
    786         // Add free-form node-related actions.
    787         contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
    788         contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
    789         contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this));
    790 
    791         if (Preferences.nativeInstrumentationEnabled) {
    792             // Add debbuging-related actions
    793             contextMenu.appendSeparator();
    794             var pane = WebInspector.panels.elements.sidebarPanes.domBreakpoints;
    795             pane.populateNodeContextMenu(this.representedObject, contextMenu);
    796         }
    797     },
    798 
    799     _populateTextContextMenu: function(contextMenu, textNode)
    800     {
    801         contextMenu.appendItem(WebInspector.UIString("Edit Text"), this._startEditingTextNode.bind(this, textNode));
    802     },
    803 
    804     _startEditing: function()
    805     {
    806         if (this.treeOutline.focusedDOMNode !== this.representedObject)
    807             return;
    808 
    809         var listItem = this._listItemNode;
    810 
    811         if (this._canAddAttributes) {
    812             var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
    813             if (attribute)
    814                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]);
    815 
    816             return this._addNewAttribute();
    817         }
    818 
    819         if (this.representedObject.nodeType() === Node.TEXT_NODE) {
    820             var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0];
    821             if (textNode)
    822                 return this._startEditingTextNode(textNode);
    823             return;
    824         }
    825     },
    826 
    827     _addNewAttribute: function()
    828     {
    829         // Cannot just convert the textual html into an element without
    830         // a parent node. Use a temporary span container for the HTML.
    831         var container = document.createElement("span");
    832         container.innerHTML = this._attributeHTML(" ", "");
    833         var attr = container.firstChild;
    834         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
    835         attr.style.marginRight = "2px"; // overrides the .editing margin rule
    836 
    837         var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0];
    838         this._insertInLastAttributePosition(tag, attr);
    839         return this._startEditingAttribute(attr, attr);
    840     },
    841 
    842     _triggerEditAttribute: function(attributeName)
    843     {
    844         var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name");
    845         for (var i = 0, len = attributeElements.length; i < len; ++i) {
    846             if (attributeElements[i].textContent === attributeName) {
    847                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
    848                     if (elem.nodeType !== Node.ELEMENT_NODE)
    849                         continue;
    850 
    851                     if (elem.hasStyleClass("webkit-html-attribute-value"))
    852                         return this._startEditingAttribute(elem.parentNode, elem);
    853                 }
    854             }
    855         }
    856     },
    857 
    858     _startEditingAttribute: function(attribute, elementForSelection)
    859     {
    860         if (WebInspector.isBeingEdited(attribute))
    861             return true;
    862 
    863         var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
    864         if (!attributeNameElement)
    865             return false;
    866 
    867         var attributeName = attributeNameElement.textContent;
    868 
    869         function removeZeroWidthSpaceRecursive(node)
    870         {
    871             if (node.nodeType === Node.TEXT_NODE) {
    872                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
    873                 return;
    874             }
    875 
    876             if (node.nodeType !== Node.ELEMENT_NODE)
    877                 return;
    878 
    879             for (var child = node.firstChild; child; child = child.nextSibling)
    880                 removeZeroWidthSpaceRecursive(child);
    881         }
    882 
    883         // Remove zero-width spaces that were added by nodeTitleInfo.
    884         removeZeroWidthSpaceRecursive(attribute);
    885 
    886         this._editing = WebInspector.startEditing(attribute, {
    887             context: attributeName,
    888             commitHandler: this._attributeEditingCommitted.bind(this),
    889             cancelHandler: this._editingCancelled.bind(this)
    890         });
    891         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
    892 
    893         return true;
    894     },
    895 
    896     _startEditingTextNode: function(textNode)
    897     {
    898         if (WebInspector.isBeingEdited(textNode))
    899             return true;
    900 
    901         this._editing = WebInspector.startEditing(textNode, {
    902             context: null,
    903             commitHandler: this._textNodeEditingCommitted.bind(this),
    904             cancelHandler: this._editingCancelled.bind(this)
    905         });
    906         window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
    907 
    908         return true;
    909     },
    910 
    911     _startEditingTagName: function(tagNameElement)
    912     {
    913         if (!tagNameElement) {
    914             tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0];
    915             if (!tagNameElement)
    916                 return false;
    917         }
    918 
    919         var tagName = tagNameElement.textContent;
    920         if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()])
    921             return false;
    922 
    923         if (WebInspector.isBeingEdited(tagNameElement))
    924             return true;
    925 
    926         var closingTagElement = this._distinctClosingTagElement();
    927 
    928         function keyupListener(event)
    929         {
    930             if (closingTagElement)
    931                 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
    932         }
    933 
    934         function editingComitted(element, newTagName)
    935         {
    936             tagNameElement.removeEventListener('keyup', keyupListener, false);
    937             this._tagNameEditingCommitted.apply(this, arguments);
    938         }
    939 
    940         function editingCancelled()
    941         {
    942             tagNameElement.removeEventListener('keyup', keyupListener, false);
    943             this._editingCancelled.apply(this, arguments);
    944         }
    945 
    946         tagNameElement.addEventListener('keyup', keyupListener, false);
    947 
    948         this._editing = WebInspector.startEditing(tagNameElement, {
    949             context: tagName,
    950             commitHandler: editingComitted.bind(this),
    951             cancelHandler: editingCancelled.bind(this)
    952         });
    953         window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
    954         return true;
    955     },
    956 
    957     _startEditingAsHTML: function(commitCallback, error, initialValue)
    958     {
    959         if (error)
    960             return;
    961         if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement))
    962             return;
    963 
    964         this._htmlEditElement = document.createElement("div");
    965         this._htmlEditElement.className = "source-code elements-tree-editor";
    966         this._htmlEditElement.textContent = initialValue;
    967 
    968         // Hide header items.
    969         var child = this.listItemElement.firstChild;
    970         while (child) {
    971             child.style.display = "none";
    972             child = child.nextSibling;
    973         }
    974         // Hide children item.
    975         if (this._childrenListNode)
    976             this._childrenListNode.style.display = "none";
    977         // Append editor.
    978         this.listItemElement.appendChild(this._htmlEditElement);
    979 
    980         this.updateSelection();
    981 
    982         function commit()
    983         {
    984             commitCallback(this._htmlEditElement.textContent);
    985             dispose.call(this);
    986         }
    987 
    988         function dispose()
    989         {
    990             delete this._editing;
    991 
    992             // Remove editor.
    993             this.listItemElement.removeChild(this._htmlEditElement);
    994             delete this._htmlEditElement;
    995             // Unhide children item.
    996             if (this._childrenListNode)
    997                 this._childrenListNode.style.removeProperty("display");
    998             // Unhide header items.
    999             var child = this.listItemElement.firstChild;
   1000             while (child) {
   1001                 child.style.removeProperty("display");
   1002                 child = child.nextSibling;
   1003             }
   1004 
   1005             this.updateSelection();
   1006         }
   1007 
   1008         this._editing = WebInspector.startEditing(this._htmlEditElement, {
   1009             context: null,
   1010             commitHandler: commit.bind(this),
   1011             cancelHandler: dispose.bind(this),
   1012             multiline: true
   1013         });
   1014     },
   1015 
   1016     _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
   1017     {
   1018         delete this._editing;
   1019 
   1020         // Before we do anything, determine where we should move
   1021         // next based on the current element's settings
   1022         var moveToAttribute, moveToTagName, moveToNewAttribute;
   1023         if (moveDirection) {
   1024             var found = false;
   1025 
   1026             // Search for the attribute's position, and then decide where to move to.
   1027             var attributes = this.representedObject.attributes();
   1028             for (var i = 0; i < attributes.length; ++i) {
   1029                 if (attributes[i].name === attributeName) {
   1030                     found = true;
   1031                     if (moveDirection === "backward") {
   1032                         if (i === 0)
   1033                             moveToTagName = true;
   1034                         else
   1035                             moveToAttribute = attributes[i - 1].name;
   1036                     } else if (moveDirection === "forward") {
   1037                         if (i === attributes.length - 1)
   1038                             moveToNewAttribute = true;
   1039                         else
   1040                             moveToAttribute = attributes[i + 1].name;
   1041                     }
   1042                 }
   1043             }
   1044 
   1045             // Moving From the "New Attribute" position.
   1046             if (!found) {
   1047                 if (moveDirection === "backward" && attributes.length > 0)
   1048                     moveToAttribute = attributes[attributes.length - 1].name;
   1049                 else if (moveDirection === "forward") {
   1050                     if (!/^\s*$/.test(newText))
   1051                         moveToNewAttribute = true;
   1052                     else
   1053                         moveToTagName = true;
   1054                 }
   1055             }
   1056         }
   1057 
   1058         function moveToNextAttributeIfNeeded()
   1059         {
   1060             // Cleanup empty new attribute sections.
   1061             if (element.textContent.trim().length === 0)
   1062                 element.parentNode.removeChild(element);
   1063 
   1064             // Make the move.
   1065             if (moveToAttribute)
   1066                 this._triggerEditAttribute(moveToAttribute);
   1067             else if (moveToNewAttribute)
   1068                 this._addNewAttribute();
   1069             else if (moveToTagName)
   1070                 this._startEditingTagName();
   1071         }
   1072 
   1073         function regenerateStyledAttribute(name, value)
   1074         {
   1075             var previous = element.previousSibling;
   1076             if (!previous || previous.nodeType !== Node.TEXT_NODE)
   1077                 element.parentNode.insertBefore(document.createTextNode(" "), element);
   1078             element.outerHTML = this._attributeHTML(name, value);
   1079         }
   1080 
   1081         var parseContainerElement = document.createElement("span");
   1082         parseContainerElement.innerHTML = "<span " + newText + "></span>";
   1083         var parseElement = parseContainerElement.firstChild;
   1084 
   1085         if (!parseElement) {
   1086             this._editingCancelled(element, attributeName);
   1087             moveToNextAttributeIfNeeded.call(this);
   1088             return;
   1089         }
   1090 
   1091         if (!parseElement.hasAttributes()) {
   1092             this.representedObject.removeAttribute(attributeName, this.updateTitle.bind(this));
   1093             this.treeOutline.focusedNodeChanged(true);
   1094             moveToNextAttributeIfNeeded.call(this);
   1095             return;
   1096         }
   1097 
   1098         var foundOriginalAttribute = false;
   1099         for (var i = 0; i < parseElement.attributes.length; ++i) {
   1100             var attr = parseElement.attributes[i];
   1101             foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName;
   1102             try {
   1103                 this.representedObject.setAttribute(attr.name, attr.value, this.updateTitle.bind(this));
   1104                 regenerateStyledAttribute.call(this, attr.name, attr.value);
   1105             } catch(e) {} // ignore invalid attribute (innerHTML doesn't throw errors, but this can)
   1106         }
   1107 
   1108         if (!foundOriginalAttribute)
   1109             this.representedObject.removeAttribute(attributeName, this.updateTitle.bind(this));
   1110 
   1111         this.treeOutline.focusedNodeChanged(true);
   1112 
   1113         moveToNextAttributeIfNeeded.call(this);
   1114     },
   1115 
   1116     _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
   1117     {
   1118         delete this._editing;
   1119         var self = this;
   1120 
   1121         function cancel()
   1122         {
   1123             var closingTagElement = self._distinctClosingTagElement();
   1124             if (closingTagElement)
   1125                 closingTagElement.textContent = "</" + tagName + ">";
   1126 
   1127             self._editingCancelled(element, tagName);
   1128             moveToNextAttributeIfNeeded.call(self);
   1129         }
   1130 
   1131         function moveToNextAttributeIfNeeded()
   1132         {
   1133             if (moveDirection !== "forward") {
   1134                 this._addNewAttribute();
   1135                 return;
   1136             }
   1137 
   1138             var attributes = this.representedObject.attributes();
   1139             if (attributes.length > 0)
   1140                 this._triggerEditAttribute(attributes[0].name);
   1141             else
   1142                 this._addNewAttribute();
   1143         }
   1144 
   1145         newText = newText.trim();
   1146         if (newText === oldText) {
   1147             cancel();
   1148             return;
   1149         }
   1150 
   1151         var treeOutline = this.treeOutline;
   1152         var wasExpanded = this.expanded;
   1153 
   1154         function changeTagNameCallback(error, nodeId)
   1155         {
   1156             if (error || !nodeId) {
   1157                 cancel();
   1158                 return;
   1159             }
   1160 
   1161             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
   1162             WebInspector.panels.elements.updateModifiedNodes();
   1163 
   1164             WebInspector.updateFocusedNode(nodeId);
   1165             var newTreeItem = treeOutline.findTreeElement(WebInspector.domAgent.nodeForId(nodeId));
   1166             if (wasExpanded)
   1167                 newTreeItem.expand();
   1168 
   1169             moveToNextAttributeIfNeeded.call(newTreeItem);
   1170         }
   1171 
   1172         this.representedObject.setNodeName(newText, changeTagNameCallback);
   1173     },
   1174 
   1175     _textNodeEditingCommitted: function(element, newText)
   1176     {
   1177         delete this._editing;
   1178 
   1179         var textNode;
   1180         if (this.representedObject.nodeType() === Node.ELEMENT_NODE) {
   1181             // We only show text nodes inline in elements if the element only
   1182             // has a single child, and that child is a text node.
   1183             textNode = this.representedObject.firstChild;
   1184         } else if (this.representedObject.nodeType() == Node.TEXT_NODE)
   1185             textNode = this.representedObject;
   1186 
   1187         textNode.setNodeValue(newText, this.updateTitle.bind(this));
   1188     },
   1189 
   1190     _editingCancelled: function(element, context)
   1191     {
   1192         delete this._editing;
   1193 
   1194         // Need to restore attributes structure.
   1195         this.updateTitle();
   1196     },
   1197 
   1198     _distinctClosingTagElement: function()
   1199     {
   1200         // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
   1201 
   1202         // For an expanded element, it will be the last element with class "close"
   1203         // in the child element list.
   1204         if (this.expanded) {
   1205             var closers = this._childrenListNode.querySelectorAll(".close");
   1206             return closers[closers.length-1];
   1207         }
   1208 
   1209         // Remaining cases are single line non-expanded elements with a closing
   1210         // tag, or HTML elements without a closing tag (such as <br>). Return
   1211         // null in the case where there isn't a closing tag.
   1212         var tags = this.listItemElement.getElementsByClassName("webkit-html-tag");
   1213         return (tags.length === 1 ? null : tags[tags.length-1]);
   1214     },
   1215 
   1216     updateTitle: function(onlySearchQueryChanged)
   1217     {
   1218         // If we are editing, return early to prevent canceling the edit.
   1219         // After editing is committed updateTitle will be called.
   1220         if (this._editing)
   1221             return;
   1222 
   1223         if (onlySearchQueryChanged && this._normalHTML)
   1224             this.titleHTML = this._normalHTML;
   1225         else {
   1226             delete this._normalHTML;
   1227             this.titleHTML = "<span class=\"highlight\">" + this._nodeTitleInfo(WebInspector.linkifyURL).titleHTML + "</span>";
   1228         }
   1229 
   1230         delete this.selectionElement;
   1231         this.updateSelection();
   1232         this._preventFollowingLinksOnDoubleClick();
   1233         this._highlightSearchResults();
   1234     },
   1235 
   1236     _attributeHTML: function(name, value, node, linkify)
   1237     {
   1238         var hasText = (value.length > 0);
   1239         var html = "<span class=\"webkit-html-attribute\"><span class=\"webkit-html-attribute-name\">" + name.escapeHTML() + "</span>";
   1240 
   1241         if (hasText)
   1242             html += "=&#8203;\"";
   1243 
   1244         if (linkify && (name === "src" || name === "href")) {
   1245             var rewrittenHref = WebInspector.resourceURLForRelatedNode(node, value);
   1246             value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B");
   1247             html += linkify(rewrittenHref, value, "webkit-html-attribute-value", node.nodeName().toLowerCase() === "a");
   1248         } else {
   1249             value = value.escapeHTML().replace(/([\/;:\)\]\}])/g, "$1&#8203;");
   1250             html += "<span class=\"webkit-html-attribute-value\">" + value + "</span>";
   1251         }
   1252 
   1253         if (hasText)
   1254             html += "\"";
   1255 
   1256         html += "</span>";
   1257         return html;
   1258     },
   1259 
   1260     _tagHTML: function(tagName, isClosingTag, isDistinctTreeElement, linkify)
   1261     {
   1262         var node = this.representedObject;
   1263         var result = "<span class=\"webkit-html-tag" + (isClosingTag && isDistinctTreeElement ? " close" : "")  + "\">&lt;";
   1264         result += "<span " + (isClosingTag ? "" : "class=\"webkit-html-tag-name\"") + ">" + (isClosingTag ? "/" : "") + tagName + "</span>";
   1265         if (!isClosingTag && node.hasAttributes()) {
   1266             var attributes = node.attributes();
   1267             for (var i = 0; i < attributes.length; ++i) {
   1268                 var attr = attributes[i];
   1269                 result += " " + this._attributeHTML(attr.name, attr.value, node, linkify);
   1270             }
   1271         }
   1272         result += "&gt;</span>&#8203;";
   1273 
   1274         return result;
   1275     },
   1276 
   1277     _nodeTitleInfo: function(linkify)
   1278     {
   1279         var node = this.representedObject;
   1280         var info = {titleHTML: "", hasChildren: this.hasChildren};
   1281 
   1282         switch (node.nodeType()) {
   1283             case Node.DOCUMENT_NODE:
   1284                 info.titleHTML = "Document";
   1285                 break;
   1286 
   1287             case Node.DOCUMENT_FRAGMENT_NODE:
   1288                 info.titleHTML = "Document Fragment";
   1289                 break;
   1290 
   1291             case Node.ATTRIBUTE_NODE:
   1292                 var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
   1293                 info.titleHTML = this._attributeHTML(node.name, value);
   1294                 break;
   1295 
   1296             case Node.ELEMENT_NODE:
   1297                 var tagName = this.treeOutline.nodeNameToCorrectCase(node.nodeName()).escapeHTML();
   1298                 if (this._elementCloseTag) {
   1299                     info.titleHTML = this._tagHTML(tagName, true, true);
   1300                     info.hasChildren = false;
   1301                     break;
   1302                 }
   1303 
   1304                 var titleHTML = this._tagHTML(tagName, false, false, linkify);
   1305 
   1306                 var textChild = this._singleTextChild(node);
   1307                 var showInlineText = textChild && textChild.nodeValue().length < Preferences.maxInlineTextChildLength;
   1308 
   1309                 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName]))) {
   1310                     if (this.hasChildren)
   1311                         titleHTML += "<span class=\"webkit-html-text-node\">&#8230;</span>&#8203;";
   1312                     titleHTML += this._tagHTML(tagName, true, false);
   1313                 }
   1314 
   1315                 // If this element only has a single child that is a text node,
   1316                 // just show that text and the closing tag inline rather than
   1317                 // create a subtree for them
   1318                 if (showInlineText) {
   1319                     titleHTML += "<span class=\"webkit-html-text-node\">" + textChild.nodeValue().escapeHTML() + "</span>&#8203;" + this._tagHTML(tagName, true, false);
   1320                     info.hasChildren = false;
   1321                 }
   1322                 info.titleHTML = titleHTML;
   1323                 break;
   1324 
   1325             case Node.TEXT_NODE:
   1326                 if (isNodeWhitespace.call(node))
   1327                     info.titleHTML = "(whitespace)";
   1328                 else {
   1329                     if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
   1330                         var newNode = document.createElement("span");
   1331                         newNode.textContent = node.nodeValue();
   1332 
   1333                         var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript");
   1334                         javascriptSyntaxHighlighter.syntaxHighlightNode(newNode);
   1335 
   1336                         info.titleHTML = "<span class=\"webkit-html-text-node webkit-html-js-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>";
   1337                     } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
   1338                         var newNode = document.createElement("span");
   1339                         newNode.textContent = node.nodeValue();
   1340 
   1341                         var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css");
   1342                         cssSyntaxHighlighter.syntaxHighlightNode(newNode);
   1343 
   1344                         info.titleHTML = "<span class=\"webkit-html-text-node webkit-html-css-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>";
   1345                     } else
   1346                         info.titleHTML = "\"<span class=\"webkit-html-text-node\">" + node.nodeValue().escapeHTML() + "</span>\"";
   1347                 }
   1348                 break;
   1349 
   1350             case Node.COMMENT_NODE:
   1351                 info.titleHTML = "<span class=\"webkit-html-comment\">&lt;!--" + node.nodeValue().escapeHTML() + "--&gt;</span>";
   1352                 break;
   1353 
   1354             case Node.DOCUMENT_TYPE_NODE:
   1355                 var titleHTML = "<span class=\"webkit-html-doctype\">&lt;!DOCTYPE " + node.nodeName();
   1356                 if (node.publicId) {
   1357                     titleHTML += " PUBLIC \"" + node.publicId + "\"";
   1358                     if (node.systemId)
   1359                         titleHTML += " \"" + node.systemId + "\"";
   1360                 } else if (node.systemId)
   1361                     titleHTML += " SYSTEM \"" + node.systemId + "\"";
   1362                 if (node.internalSubset)
   1363                     titleHTML += " [" + node.internalSubset + "]";
   1364                 titleHTML += "&gt;</span>";
   1365                 info.titleHTML = titleHTML;
   1366                 break;
   1367 
   1368             case Node.CDATA_SECTION_NODE:
   1369                 info.titleHTML = "<span class=\"webkit-html-text-node\">&lt;![CDATA[" + node.nodeValue().escapeHTML() + "]]&gt;</span>";
   1370                 break;
   1371             default:
   1372                 info.titleHTML = this.treeOutline.nodeNameToCorrectCase(node.nodeName()).collapseWhitespace().escapeHTML();
   1373         }
   1374 
   1375         return info;
   1376     },
   1377 
   1378     _singleTextChild: function(node)
   1379     {
   1380         if (!node)
   1381             return null;
   1382 
   1383         var firstChild = node.firstChild;
   1384         if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE)
   1385             return null;
   1386 
   1387         var sibling = firstChild.nextSibling;
   1388         return sibling ? null : firstChild;
   1389     },
   1390 
   1391     _showInlineText: function(node)
   1392     {
   1393         if (node.nodeType() === Node.ELEMENT_NODE) {
   1394             var textChild = this._singleTextChild(node);
   1395             if (textChild && textChild.nodeValue().length < Preferences.maxInlineTextChildLength)
   1396                 return true;
   1397         }
   1398         return false;
   1399     },
   1400 
   1401     remove: function()
   1402     {
   1403         var parentElement = this.parent;
   1404         if (!parentElement)
   1405             return;
   1406 
   1407         var self = this;
   1408         function removeNodeCallback(error, removedNodeId)
   1409         {
   1410             if (error)
   1411                 return;
   1412 
   1413             parentElement.removeChild(self);
   1414             parentElement.adjustCollapsedRange(true);
   1415         }
   1416 
   1417         this.representedObject.removeNode(removeNodeCallback);
   1418     },
   1419 
   1420     _editAsHTML: function()
   1421     {
   1422         var treeOutline = this.treeOutline;
   1423         var node = this.representedObject;
   1424         var wasExpanded = this.expanded;
   1425 
   1426         function selectNode(error, nodeId)
   1427         {
   1428             if (error || !nodeId)
   1429                 return;
   1430 
   1431             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
   1432             WebInspector.panels.elements.updateModifiedNodes();
   1433 
   1434             WebInspector.updateFocusedNode(nodeId);
   1435             if (wasExpanded) {
   1436                 var newTreeItem = treeOutline.findTreeElement(WebInspector.domAgent.nodeForId(nodeId));
   1437                 if (newTreeItem)
   1438                     newTreeItem.expand();
   1439             }
   1440         }
   1441 
   1442         function commitChange(value)
   1443         {
   1444             node.setOuterHTML(value, selectNode);
   1445         }
   1446 
   1447         node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
   1448     },
   1449 
   1450     _copyHTML: function()
   1451     {
   1452         this.representedObject.copyNode();
   1453     },
   1454 
   1455     _highlightSearchResults: function()
   1456     {
   1457         if (!this._searchQuery)
   1458             return;
   1459         if (this._searchHighlightedHTML) {
   1460             this.listItemElement.innerHTML = this._searchHighlightedHTML;
   1461             return;
   1462         }
   1463 
   1464         if (!this._normalHTML)
   1465             this._normalHTML = this.titleHTML;
   1466 
   1467         var text = this.listItemElement.textContent;
   1468         var regexObject = createSearchRegex(this._searchQuery, "g");
   1469 
   1470         var offset = 0;
   1471         var match = regexObject.exec(text);
   1472         var matchRanges = [];
   1473         while (match) {
   1474             matchRanges.push({ offset: match.index, length: match[0].length });
   1475             match = regexObject.exec(text);
   1476         }
   1477 
   1478         // Fall back for XPath, etc. matches.
   1479         if (!matchRanges.length)
   1480             matchRanges.push({ offset: 0, length: text.length });
   1481 
   1482         highlightSearchResults(this.listItemElement, matchRanges);
   1483         this._searchHighlightedHTML = this.listItemElement.innerHTML;
   1484     }
   1485 }
   1486 
   1487 WebInspector.ElementsTreeElement.prototype.__proto__ = TreeElement.prototype;
   1488