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 /**
     32  * @constructor
     33  * @extends {TreeOutline}
     34  * @param {boolean=} omitRootDOMNode
     35  * @param {boolean=} selectEnabled
     36  * @param {function(!WebInspector.ContextMenu, !WebInspector.DOMNode)=} contextMenuCallback
     37  * @param {function(!DOMAgent.NodeId, string, boolean)=} setPseudoClassCallback
     38  */
     39 WebInspector.ElementsTreeOutline = function(omitRootDOMNode, selectEnabled, contextMenuCallback, setPseudoClassCallback)
     40 {
     41     this.element = document.createElement("ol");
     42     this.element.className = "elements-tree-outline";
     43     this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
     44     this.element.addEventListener("mousemove", this._onmousemove.bind(this), false);
     45     this.element.addEventListener("mouseout", this._onmouseout.bind(this), false);
     46     this.element.addEventListener("dragstart", this._ondragstart.bind(this), false);
     47     this.element.addEventListener("dragover", this._ondragover.bind(this), false);
     48     this.element.addEventListener("dragleave", this._ondragleave.bind(this), false);
     49     this.element.addEventListener("drop", this._ondrop.bind(this), false);
     50     this.element.addEventListener("dragend", this._ondragend.bind(this), false);
     51     this.element.addEventListener("keydown", this._onkeydown.bind(this), false);
     52 
     53     TreeOutline.call(this, this.element);
     54 
     55     this._includeRootDOMNode = !omitRootDOMNode;
     56     this._selectEnabled = selectEnabled;
     57     /** @type {?WebInspector.DOMNode} */
     58     this._rootDOMNode = null;
     59     /** @type {?WebInspector.DOMNode} */
     60     this._selectedDOMNode = null;
     61     this._eventSupport = new WebInspector.Object();
     62 
     63     this._visible = false;
     64 
     65     this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
     66     this._contextMenuCallback = contextMenuCallback;
     67     this._setPseudoClassCallback = setPseudoClassCallback;
     68     this._createNodeDecorators();
     69 }
     70 
     71 /**
     72  * @enum {string}
     73  */
     74 WebInspector.ElementsTreeOutline.Events = {
     75     SelectedNodeChanged: "SelectedNodeChanged",
     76     ElementsTreeUpdated: "ElementsTreeUpdated"
     77 }
     78 
     79 /**
     80  * @const
     81  * @type {!Object.<string, string>}
     82  */
     83 WebInspector.ElementsTreeOutline.MappedCharToEntity = {
     84     "\u00a0": "nbsp",
     85     "\u2002": "ensp",
     86     "\u2003": "emsp",
     87     "\u2009": "thinsp",
     88     "\u200a": "#8202", // Hairspace
     89     "\u200b": "#8203", // ZWSP
     90     "\u200c": "zwnj",
     91     "\u200d": "zwj",
     92     "\u200e": "lrm",
     93     "\u200f": "rlm",
     94     "\u202a": "#8234", // LRE
     95     "\u202b": "#8235", // RLE
     96     "\u202c": "#8236", // PDF
     97     "\u202d": "#8237", // LRO
     98     "\u202e": "#8238" // RLO
     99 }
    100 
    101 WebInspector.ElementsTreeOutline.prototype = {
    102     /**
    103      * @param {number} width
    104      */
    105     setVisibleWidth: function(width)
    106     {
    107         this._visibleWidth = width;
    108         if (this._multilineEditing)
    109             this._multilineEditing.setWidth(this._visibleWidth);
    110     },
    111 
    112     _createNodeDecorators: function()
    113     {
    114         this._nodeDecorators = [];
    115         this._nodeDecorators.push(new WebInspector.ElementsTreeOutline.PseudoStateDecorator());
    116     },
    117 
    118     wireToDomAgent: function()
    119     {
    120         this._elementsTreeUpdater = new WebInspector.ElementsTreeUpdater(this);
    121     },
    122 
    123     /**
    124      * @param {boolean} visible
    125      */
    126     setVisible: function(visible)
    127     {
    128         this._visible = visible;
    129         if (!this._visible)
    130             return;
    131 
    132         this._updateModifiedNodes();
    133         if (this._selectedDOMNode)
    134             this._revealAndSelectNode(this._selectedDOMNode, false);
    135     },
    136 
    137     addEventListener: function(eventType, listener, thisObject)
    138     {
    139         this._eventSupport.addEventListener(eventType, listener, thisObject);
    140     },
    141 
    142     removeEventListener: function(eventType, listener, thisObject)
    143     {
    144         this._eventSupport.removeEventListener(eventType, listener, thisObject);
    145     },
    146 
    147     get rootDOMNode()
    148     {
    149         return this._rootDOMNode;
    150     },
    151 
    152     set rootDOMNode(x)
    153     {
    154         if (this._rootDOMNode === x)
    155             return;
    156 
    157         this._rootDOMNode = x;
    158 
    159         this._isXMLMimeType = x && x.isXMLNode();
    160 
    161         this.update();
    162     },
    163 
    164     get isXMLMimeType()
    165     {
    166         return this._isXMLMimeType;
    167     },
    168 
    169     /**
    170      * @return {?WebInspector.DOMNode}
    171      */
    172     selectedDOMNode: function()
    173     {
    174         return this._selectedDOMNode;
    175     },
    176 
    177     /**
    178      * @param {?WebInspector.DOMNode} node
    179      * @param {boolean=} focus
    180      */
    181     selectDOMNode: function(node, focus)
    182     {
    183         if (this._selectedDOMNode === node) {
    184             this._revealAndSelectNode(node, !focus);
    185             return;
    186         }
    187 
    188         this._selectedDOMNode = node;
    189         this._revealAndSelectNode(node, !focus);
    190 
    191         // The _revealAndSelectNode() method might find a different element if there is inlined text,
    192         // and the select() call would change the selectedDOMNode and reenter this setter. So to
    193         // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
    194         // node as the one passed in.
    195         if (this._selectedDOMNode === node)
    196             this._selectedNodeChanged();
    197     },
    198 
    199     /**
    200      * @return {boolean}
    201      */
    202     editing: function()
    203     {
    204         var node = this.selectedDOMNode();
    205         if (!node)
    206             return false;
    207         var treeElement = this.findTreeElement(node);
    208         if (!treeElement)
    209             return false;
    210         return treeElement._editing || false;
    211     },
    212 
    213     update: function()
    214     {
    215         var selectedNode = this.selectedTreeElement ? this.selectedTreeElement._node : null;
    216 
    217         this.removeChildren();
    218 
    219         if (!this.rootDOMNode)
    220             return;
    221 
    222         var treeElement;
    223         if (this._includeRootDOMNode) {
    224             treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode);
    225             treeElement.selectable = this._selectEnabled;
    226             this.appendChild(treeElement);
    227         } else {
    228             // FIXME: this could use findTreeElement to reuse a tree element if it already exists
    229             var node = this.rootDOMNode.firstChild;
    230             while (node) {
    231                 treeElement = new WebInspector.ElementsTreeElement(node);
    232                 treeElement.selectable = this._selectEnabled;
    233                 this.appendChild(treeElement);
    234                 node = node.nextSibling;
    235             }
    236         }
    237 
    238         if (selectedNode)
    239             this._revealAndSelectNode(selectedNode, true);
    240     },
    241 
    242     updateSelection: function()
    243     {
    244         if (!this.selectedTreeElement)
    245             return;
    246         var element = this.treeOutline.selectedTreeElement;
    247         element.updateSelection();
    248     },
    249 
    250     /**
    251      * @param {!WebInspector.DOMNode} node
    252      */
    253     updateOpenCloseTags: function(node)
    254     {
    255         var treeElement = this.findTreeElement(node);
    256         if (treeElement)
    257             treeElement.updateTitle();
    258         var children = treeElement.children;
    259         var closingTagElement = children[children.length - 1];
    260         if (closingTagElement && closingTagElement._elementCloseTag)
    261             closingTagElement.updateTitle();
    262     },
    263 
    264     _selectedNodeChanged: function()
    265     {
    266         this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedDOMNode);
    267     },
    268 
    269     /**
    270      * @param {!Array.<!WebInspector.DOMNode>} nodes
    271      */
    272     _fireElementsTreeUpdated: function(nodes)
    273     {
    274         this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.ElementsTreeUpdated, nodes);
    275     },
    276 
    277     /**
    278      * @param {!WebInspector.DOMNode} node
    279      * @return {?TreeElement}
    280      */
    281     findTreeElement: function(node)
    282     {
    283         function isAncestorNode(ancestor, node)
    284         {
    285             return ancestor.isAncestor(node);
    286         }
    287 
    288         function parentNode(node)
    289         {
    290             return node.parentNode;
    291         }
    292 
    293         var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode);
    294         if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
    295             // The text node might have been inlined if it was short, so try to find the parent element.
    296             treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode);
    297         }
    298 
    299         return treeElement;
    300     },
    301 
    302     /**
    303      * @param {!WebInspector.DOMNode} node
    304      * @return {?TreeElement}
    305      */
    306     createTreeElementFor: function(node)
    307     {
    308         var treeElement = this.findTreeElement(node);
    309         if (treeElement)
    310             return treeElement;
    311         if (!node.parentNode)
    312             return null;
    313 
    314         treeElement = this.createTreeElementFor(node.parentNode);
    315         return treeElement ? treeElement._showChild(node) : null;
    316     },
    317 
    318     set suppressRevealAndSelect(x)
    319     {
    320         if (this._suppressRevealAndSelect === x)
    321             return;
    322         this._suppressRevealAndSelect = x;
    323     },
    324 
    325     /**
    326      * @param {?WebInspector.DOMNode} node
    327      * @param {boolean} omitFocus
    328      */
    329     _revealAndSelectNode: function(node, omitFocus)
    330     {
    331         if (this._suppressRevealAndSelect)
    332             return;
    333 
    334         if (!this._includeRootDOMNode && node === this.rootDOMNode && this.rootDOMNode)
    335             node = this.rootDOMNode.firstChild;
    336         if (!node)
    337             return;
    338         var treeElement = this.createTreeElementFor(node);
    339         if (!treeElement)
    340             return;
    341 
    342         treeElement.revealAndSelect(omitFocus);
    343     },
    344 
    345     /**
    346      * @return {?TreeElement}
    347      */
    348     _treeElementFromEvent: function(event)
    349     {
    350         var scrollContainer = this.element.parentElement;
    351 
    352         // We choose this X coordinate based on the knowledge that our list
    353         // items extend at least to the right edge of the outer <ol> container.
    354         // In the no-word-wrap mode the outer <ol> may be wider than the tree container
    355         // (and partially hidden), in which case we are left to use only its right boundary.
    356         var x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36;
    357 
    358         var y = event.pageY;
    359 
    360         // Our list items have 1-pixel cracks between them vertically. We avoid
    361         // the cracks by checking slightly above and slightly below the mouse
    362         // and seeing if we hit the same element each time.
    363         var elementUnderMouse = this.treeElementFromPoint(x, y);
    364         var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
    365         var element;
    366         if (elementUnderMouse === elementAboveMouse)
    367             element = elementUnderMouse;
    368         else
    369             element = this.treeElementFromPoint(x, y + 2);
    370 
    371         return element;
    372     },
    373 
    374     _onmousedown: function(event)
    375     {
    376         var element = this._treeElementFromEvent(event);
    377 
    378         if (!element || element.isEventWithinDisclosureTriangle(event))
    379             return;
    380 
    381         element.select();
    382     },
    383 
    384     _onmousemove: function(event)
    385     {
    386         var element = this._treeElementFromEvent(event);
    387         if (element && this._previousHoveredElement === element)
    388             return;
    389 
    390         if (this._previousHoveredElement) {
    391             this._previousHoveredElement.hovered = false;
    392             delete this._previousHoveredElement;
    393         }
    394 
    395         if (element) {
    396             element.hovered = true;
    397             this._previousHoveredElement = element;
    398         }
    399 
    400         WebInspector.domAgent.highlightDOMNode(element && element._node ? element._node.id : 0);
    401     },
    402 
    403     _onmouseout: function(event)
    404     {
    405         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
    406         if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
    407             return;
    408 
    409         if (this._previousHoveredElement) {
    410             this._previousHoveredElement.hovered = false;
    411             delete this._previousHoveredElement;
    412         }
    413 
    414         WebInspector.domAgent.hideDOMNodeHighlight();
    415     },
    416 
    417     _ondragstart: function(event)
    418     {
    419         if (!window.getSelection().isCollapsed)
    420             return false;
    421         if (event.target.nodeName === "A")
    422             return false;
    423 
    424         var treeElement = this._treeElementFromEvent(event);
    425         if (!treeElement)
    426             return false;
    427 
    428         if (!this._isValidDragSourceOrTarget(treeElement))
    429             return false;
    430 
    431         if (treeElement._node.nodeName() === "BODY" || treeElement._node.nodeName() === "HEAD")
    432             return false;
    433 
    434         event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent);
    435         event.dataTransfer.effectAllowed = "copyMove";
    436         this._treeElementBeingDragged = treeElement;
    437 
    438         WebInspector.domAgent.hideDOMNodeHighlight();
    439 
    440         return true;
    441     },
    442 
    443     _ondragover: function(event)
    444     {
    445         if (!this._treeElementBeingDragged)
    446             return false;
    447 
    448         var treeElement = this._treeElementFromEvent(event);
    449         if (!this._isValidDragSourceOrTarget(treeElement))
    450             return false;
    451 
    452         var node = treeElement._node;
    453         while (node) {
    454             if (node === this._treeElementBeingDragged._node)
    455                 return false;
    456             node = node.parentNode;
    457         }
    458 
    459         treeElement.updateSelection();
    460         treeElement.listItemElement.classList.add("elements-drag-over");
    461         this._dragOverTreeElement = treeElement;
    462         event.preventDefault();
    463         event.dataTransfer.dropEffect = 'move';
    464         return false;
    465     },
    466 
    467     _ondragleave: function(event)
    468     {
    469         this._clearDragOverTreeElementMarker();
    470         event.preventDefault();
    471         return false;
    472     },
    473 
    474     /**
    475      * @param {?TreeElement} treeElement
    476      * @return {boolean}
    477      */
    478     _isValidDragSourceOrTarget: function(treeElement)
    479     {
    480         if (!treeElement)
    481             return false;
    482 
    483         var node = treeElement.representedObject;
    484         if (!(node instanceof WebInspector.DOMNode))
    485             return false;
    486 
    487         if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
    488             return false;
    489 
    490         return true;
    491     },
    492 
    493     _ondrop: function(event)
    494     {
    495         event.preventDefault();
    496         var treeElement = this._treeElementFromEvent(event);
    497         if (treeElement)
    498             this._doMove(treeElement);
    499     },
    500 
    501     /**
    502      * @param {!TreeElement} treeElement
    503      */
    504     _doMove: function(treeElement)
    505     {
    506         if (!this._treeElementBeingDragged)
    507             return;
    508 
    509         var parentNode;
    510         var anchorNode;
    511 
    512         if (treeElement._elementCloseTag) {
    513             // Drop onto closing tag -> insert as last child.
    514             parentNode = treeElement._node;
    515         } else {
    516             var dragTargetNode = treeElement._node;
    517             parentNode = dragTargetNode.parentNode;
    518             anchorNode = dragTargetNode;
    519         }
    520 
    521         var wasExpanded = this._treeElementBeingDragged.expanded;
    522         this._treeElementBeingDragged._node.moveTo(parentNode, anchorNode, this._selectNodeAfterEdit.bind(this, wasExpanded));
    523 
    524         delete this._treeElementBeingDragged;
    525     },
    526 
    527     _ondragend: function(event)
    528     {
    529         event.preventDefault();
    530         this._clearDragOverTreeElementMarker();
    531         delete this._treeElementBeingDragged;
    532     },
    533 
    534     _clearDragOverTreeElementMarker: function()
    535     {
    536         if (this._dragOverTreeElement) {
    537             this._dragOverTreeElement.updateSelection();
    538             this._dragOverTreeElement.listItemElement.classList.remove("elements-drag-over");
    539             delete this._dragOverTreeElement;
    540         }
    541     },
    542 
    543     /**
    544      * @param {?Event} event
    545      */
    546     _onkeydown: function(event)
    547     {
    548         var keyboardEvent = /** @type {!KeyboardEvent} */ (event);
    549         var node = /** @type {!WebInspector.DOMNode} */ (this.selectedDOMNode());
    550         console.assert(node);
    551         var treeElement = this.getCachedTreeElement(node);
    552         if (!treeElement)
    553             return;
    554 
    555         if (!treeElement._editing && WebInspector.KeyboardShortcut.hasNoModifiers(keyboardEvent) && keyboardEvent.keyCode === WebInspector.KeyboardShortcut.Keys.H.code) {
    556             this._toggleHideShortcut(node);
    557             event.consume(true);
    558             return;
    559         }
    560     },
    561 
    562     _contextMenuEventFired: function(event)
    563     {
    564         var treeElement = this._treeElementFromEvent(event);
    565         if (!treeElement)
    566             return;
    567 
    568         var contextMenu = new WebInspector.ContextMenu(event);
    569         contextMenu.appendApplicableItems(treeElement._node);
    570         contextMenu.show();
    571     },
    572 
    573     populateContextMenu: function(contextMenu, event)
    574     {
    575         var treeElement = this._treeElementFromEvent(event);
    576         if (!treeElement)
    577             return;
    578 
    579         var isPseudoElement = !!treeElement._node.pseudoType();
    580         var isTag = treeElement._node.nodeType() === Node.ELEMENT_NODE && !isPseudoElement;
    581         var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
    582         if (textNode && textNode.classList.contains("bogus"))
    583             textNode = null;
    584         var commentNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-comment");
    585         contextMenu.appendApplicableItems(event.target);
    586         if (textNode) {
    587             contextMenu.appendSeparator();
    588             treeElement._populateTextContextMenu(contextMenu, textNode);
    589         } else if (isTag) {
    590             contextMenu.appendSeparator();
    591             treeElement._populateTagContextMenu(contextMenu, event);
    592         } else if (commentNode) {
    593             contextMenu.appendSeparator();
    594             treeElement._populateNodeContextMenu(contextMenu, textNode);
    595         } else if (isPseudoElement) {
    596             treeElement._populateScrollIntoView(contextMenu);
    597         }
    598     },
    599 
    600     _updateModifiedNodes: function()
    601     {
    602         if (this._elementsTreeUpdater)
    603             this._elementsTreeUpdater._updateModifiedNodes();
    604     },
    605 
    606     _populateContextMenu: function(contextMenu, node)
    607     {
    608         if (this._contextMenuCallback)
    609             this._contextMenuCallback(contextMenu, node);
    610     },
    611 
    612     handleShortcut: function(event)
    613     {
    614         var node = this.selectedDOMNode();
    615         var treeElement = this.getCachedTreeElement(node);
    616         if (!node || !treeElement)
    617             return;
    618 
    619         if (event.keyIdentifier === "F2") {
    620             this._toggleEditAsHTML(node);
    621             event.handled = true;
    622             return;
    623         }
    624 
    625         if (WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) && node.parentNode) {
    626             if (event.keyIdentifier === "Up" && node.previousSibling) {
    627                 node.moveTo(node.parentNode, node.previousSibling, this._selectNodeAfterEdit.bind(this, treeElement.expanded));
    628                 event.handled = true;
    629                 return;
    630             }
    631             if (event.keyIdentifier === "Down" && node.nextSibling) {
    632                 node.moveTo(node.parentNode, node.nextSibling.nextSibling, this._selectNodeAfterEdit.bind(this, treeElement.expanded));
    633                 event.handled = true;
    634                 return;
    635             }
    636         }
    637     },
    638 
    639     /**
    640      * @param {!WebInspector.DOMNode} node
    641      */
    642     _toggleEditAsHTML: function(node)
    643     {
    644         var treeElement = this.getCachedTreeElement(node);
    645         if (!treeElement)
    646             return;
    647 
    648         if (treeElement._editing && treeElement._htmlEditElement && WebInspector.isBeingEdited(treeElement._htmlEditElement))
    649             treeElement._editing.commit();
    650         else
    651             treeElement._editAsHTML();
    652     },
    653 
    654     /**
    655      * @param {boolean} wasExpanded
    656      * @param {?Protocol.Error} error
    657      * @param {!DOMAgent.NodeId=} nodeId
    658      */
    659     _selectNodeAfterEdit: function(wasExpanded, error, nodeId)
    660     {
    661         if (error)
    662             return;
    663 
    664         // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
    665         this._updateModifiedNodes();
    666 
    667         var newNode = nodeId ? WebInspector.domAgent.nodeForId(nodeId) : null;
    668         if (!newNode)
    669             return;
    670 
    671         this.selectDOMNode(newNode, true);
    672 
    673         var newTreeItem = this.findTreeElement(newNode);
    674         if (wasExpanded) {
    675             if (newTreeItem)
    676                 newTreeItem.expand();
    677         }
    678         return newTreeItem;
    679     },
    680 
    681     /**
    682      * Runs a script on the node's remote object that toggles a class name on
    683      * the node and injects a stylesheet into the head of the node's document
    684      * containing a rule to set "visibility: hidden" on the class and all it's
    685      * ancestors.
    686      *
    687      * @param {!WebInspector.DOMNode} node
    688      * @param {function(?WebInspector.RemoteObject, boolean=)=} userCallback
    689      */
    690     _toggleHideShortcut: function(node, userCallback)
    691     {
    692         var pseudoType = node.pseudoType();
    693         var effectiveNode = pseudoType ? node.parentNode : node;
    694         if (!effectiveNode)
    695             return;
    696 
    697         function resolvedNode(object)
    698         {
    699             if (!object)
    700                 return;
    701 
    702             /**
    703              * @param {?string} pseudoType
    704              * @this {!Element}
    705              */
    706             function toggleClassAndInjectStyleRule(pseudoType)
    707             {
    708                 const classNamePrefix = "__web-inspector-hide";
    709                 const classNameSuffix = "-shortcut__";
    710                 const styleTagId = "__web-inspector-hide-shortcut-style__";
    711                 const styleRules = ".__web-inspector-hide-shortcut__, .__web-inspector-hide-shortcut__ * { visibility: hidden !important; } .__web-inspector-hidebefore-shortcut__::before { visibility: hidden !important; } .__web-inspector-hideafter-shortcut__::after { visibility: hidden !important; }";
    712 
    713                 var className = classNamePrefix + (pseudoType || "") + classNameSuffix;
    714                 this.classList.toggle(className);
    715 
    716                 var style = document.head.querySelector("style#" + styleTagId);
    717                 if (style)
    718                     return;
    719 
    720                 style = document.createElement("style");
    721                 style.id = styleTagId;
    722                 style.type = "text/css";
    723                 style.textContent = styleRules;
    724                 document.head.appendChild(style);
    725             }
    726 
    727             object.callFunction(toggleClassAndInjectStyleRule, [{ value: pseudoType }], userCallback);
    728             object.release();
    729         }
    730 
    731         WebInspector.RemoteObject.resolveNode(effectiveNode, "", resolvedNode);
    732     },
    733 
    734     __proto__: TreeOutline.prototype
    735 }
    736 
    737 WebInspector.ElementsTreeOutline.showShadowDOM = function()
    738 {
    739     return WebInspector.settings.showShadowDOM.get() || WebInspector.ElementsTreeOutline["showShadowDOMForTest"];
    740 }
    741 
    742 
    743 /**
    744  * @interface
    745  */
    746 WebInspector.ElementsTreeOutline.ElementDecorator = function()
    747 {
    748 }
    749 
    750 WebInspector.ElementsTreeOutline.ElementDecorator.prototype = {
    751     /**
    752      * @param {!WebInspector.DOMNode} node
    753      * @return {?string}
    754      */
    755     decorate: function(node)
    756     {
    757     },
    758 
    759     /**
    760      * @param {!WebInspector.DOMNode} node
    761      * @return {?string}
    762      */
    763     decorateAncestor: function(node)
    764     {
    765     }
    766 }
    767 
    768 /**
    769  * @constructor
    770  * @implements {WebInspector.ElementsTreeOutline.ElementDecorator}
    771  */
    772 WebInspector.ElementsTreeOutline.PseudoStateDecorator = function()
    773 {
    774     WebInspector.ElementsTreeOutline.ElementDecorator.call(this);
    775 }
    776 
    777 WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName = "pseudoState";
    778 
    779 WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype = {
    780     decorate: function(node)
    781     {
    782         if (node.nodeType() !== Node.ELEMENT_NODE)
    783             return null;
    784         var propertyValue = node.getUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName);
    785         if (!propertyValue)
    786             return null;
    787         return WebInspector.UIString("Element state: %s", ":" + propertyValue.join(", :"));
    788     },
    789 
    790     decorateAncestor: function(node)
    791     {
    792         if (node.nodeType() !== Node.ELEMENT_NODE)
    793             return null;
    794 
    795         var descendantCount = node.descendantUserPropertyCount(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName);
    796         if (!descendantCount)
    797             return null;
    798         if (descendantCount === 1)
    799             return WebInspector.UIString("%d descendant with forced state", descendantCount);
    800         return WebInspector.UIString("%d descendants with forced state", descendantCount);
    801     },
    802 
    803     __proto__: WebInspector.ElementsTreeOutline.ElementDecorator.prototype
    804 }
    805 
    806 /**
    807  * @constructor
    808  * @extends {TreeElement}
    809  * @param {boolean=} elementCloseTag
    810  */
    811 WebInspector.ElementsTreeElement = function(node, elementCloseTag)
    812 {
    813     // The title will be updated in onattach.
    814     TreeElement.call(this, "", node);
    815     this._node = node;
    816 
    817     this._elementCloseTag = elementCloseTag;
    818     this._updateHasChildren();
    819 
    820     if (this._node.nodeType() == Node.ELEMENT_NODE && !elementCloseTag)
    821         this._canAddAttributes = true;
    822     this._searchQuery = null;
    823     this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit;
    824 }
    825 
    826 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
    827 
    828 // A union of HTML4 and HTML5-Draft elements that explicitly
    829 // or implicitly (for HTML5) forbid the closing tag.
    830 // FIXME: Revise once HTML5 Final is published.
    831 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [
    832     "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
    833     "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source"
    834 ].keySet();
    835 
    836 // These tags we do not allow editing their tag name.
    837 WebInspector.ElementsTreeElement.EditTagBlacklist = [
    838     "html", "head", "body"
    839 ].keySet();
    840 
    841 WebInspector.ElementsTreeElement.prototype = {
    842     highlightSearchResults: function(searchQuery)
    843     {
    844         if (this._searchQuery !== searchQuery) {
    845             this._updateSearchHighlight(false);
    846             delete this._highlightResult; // A new search query.
    847         }
    848 
    849         this._searchQuery = searchQuery;
    850         this._searchHighlightsVisible = true;
    851         this.updateTitle(true);
    852     },
    853 
    854     hideSearchHighlights: function()
    855     {
    856         delete this._searchHighlightsVisible;
    857         this._updateSearchHighlight(false);
    858     },
    859 
    860     _updateSearchHighlight: function(show)
    861     {
    862         if (!this._highlightResult)
    863             return;
    864 
    865         function updateEntryShow(entry)
    866         {
    867             switch (entry.type) {
    868                 case "added":
    869                     entry.parent.insertBefore(entry.node, entry.nextSibling);
    870                     break;
    871                 case "changed":
    872                     entry.node.textContent = entry.newText;
    873                     break;
    874             }
    875         }
    876 
    877         function updateEntryHide(entry)
    878         {
    879             switch (entry.type) {
    880                 case "added":
    881                     entry.node.remove();
    882                     break;
    883                 case "changed":
    884                     entry.node.textContent = entry.oldText;
    885                     break;
    886             }
    887         }
    888 
    889         // Preserve the semantic of node by following the order of updates for hide and show.
    890         if (show) {
    891             for (var i = 0, size = this._highlightResult.length; i < size; ++i)
    892                 updateEntryShow(this._highlightResult[i]);
    893         } else {
    894             for (var i = (this._highlightResult.length - 1); i >= 0; --i)
    895                 updateEntryHide(this._highlightResult[i]);
    896         }
    897     },
    898 
    899     get hovered()
    900     {
    901         return this._hovered;
    902     },
    903 
    904     set hovered(x)
    905     {
    906         if (this._hovered === x)
    907             return;
    908 
    909         this._hovered = x;
    910 
    911         if (this.listItemElement) {
    912             if (x) {
    913                 this.updateSelection();
    914                 this.listItemElement.classList.add("hovered");
    915             } else {
    916                 this.listItemElement.classList.remove("hovered");
    917             }
    918         }
    919     },
    920 
    921     get expandedChildrenLimit()
    922     {
    923         return this._expandedChildrenLimit;
    924     },
    925 
    926     set expandedChildrenLimit(x)
    927     {
    928         if (this._expandedChildrenLimit === x)
    929             return;
    930 
    931         this._expandedChildrenLimit = x;
    932         if (this.treeOutline && !this._updateChildrenInProgress)
    933             this._updateChildren(true);
    934     },
    935 
    936     get expandedChildCount()
    937     {
    938         var count = this.children.length;
    939         if (count && this.children[count - 1]._elementCloseTag)
    940             count--;
    941         if (count && this.children[count - 1].expandAllButton)
    942             count--;
    943         return count;
    944     },
    945 
    946     /**
    947      * @param {!WebInspector.DOMNode} child
    948      * @return {?WebInspector.ElementsTreeElement}
    949      */
    950     _showChild: function(child)
    951     {
    952         if (this._elementCloseTag)
    953             return null;
    954 
    955         var index = this._visibleChildren().indexOf(child);
    956         if (index === -1)
    957             return null;
    958 
    959         if (index >= this.expandedChildrenLimit) {
    960             this._expandedChildrenLimit = index + 1;
    961             this._updateChildren(true);
    962         }
    963 
    964         // Whether index-th child is visible in the children tree
    965         return this.expandedChildCount > index ? this.children[index] : null;
    966     },
    967 
    968     updateSelection: function()
    969     {
    970         var listItemElement = this.listItemElement;
    971         if (!listItemElement)
    972             return;
    973 
    974         if (!this._readyToUpdateSelection) {
    975             if (document.body.offsetWidth > 0)
    976                 this._readyToUpdateSelection = true;
    977             else {
    978                 // The stylesheet hasn't loaded yet or the window is closed,
    979                 // so we can't calculate what we need. Return early.
    980                 return;
    981             }
    982         }
    983 
    984         if (!this.selectionElement) {
    985             this.selectionElement = document.createElement("div");
    986             this.selectionElement.className = "selection selected";
    987             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
    988         }
    989 
    990         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
    991     },
    992 
    993     onattach: function()
    994     {
    995         if (this._hovered) {
    996             this.updateSelection();
    997             this.listItemElement.classList.add("hovered");
    998         }
    999 
   1000         this.updateTitle();
   1001         this._preventFollowingLinksOnDoubleClick();
   1002         this.listItemElement.draggable = true;
   1003     },
   1004 
   1005     _preventFollowingLinksOnDoubleClick: function()
   1006     {
   1007         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");
   1008         if (!links)
   1009             return;
   1010 
   1011         for (var i = 0; i < links.length; ++i)
   1012             links[i].preventFollowOnDoubleClick = true;
   1013     },
   1014 
   1015     onpopulate: function()
   1016     {
   1017         if (this.children.length || this._showInlineText() || this._elementCloseTag)
   1018             return;
   1019 
   1020         this.updateChildren();
   1021     },
   1022 
   1023     /**
   1024      * @param {boolean=} fullRefresh
   1025      */
   1026     updateChildren: function(fullRefresh)
   1027     {
   1028         if (this._elementCloseTag)
   1029             return;
   1030         this._node.getChildNodes(this._updateChildren.bind(this, fullRefresh));
   1031     },
   1032 
   1033     /**
   1034      * @param {boolean=} closingTag
   1035      */
   1036     insertChildElement: function(child, index, closingTag)
   1037     {
   1038         var newElement = new WebInspector.ElementsTreeElement(child, closingTag);
   1039         newElement.selectable = this.treeOutline._selectEnabled;
   1040         this.insertChild(newElement, index);
   1041         return newElement;
   1042     },
   1043 
   1044     moveChild: function(child, targetIndex)
   1045     {
   1046         var wasSelected = child.selected;
   1047         this.removeChild(child);
   1048         this.insertChild(child, targetIndex);
   1049         if (wasSelected)
   1050             child.select();
   1051     },
   1052 
   1053     /**
   1054      * @param {boolean=} fullRefresh
   1055      */
   1056     _updateChildren: function(fullRefresh)
   1057     {
   1058         if (this._updateChildrenInProgress || !this.treeOutline._visible)
   1059             return;
   1060 
   1061         this._updateChildrenInProgress = true;
   1062         var selectedNode = this.treeOutline.selectedDOMNode();
   1063         var originalScrollTop = 0;
   1064         if (fullRefresh) {
   1065             var treeOutlineContainerElement = this.treeOutline.element.parentNode;
   1066             originalScrollTop = treeOutlineContainerElement.scrollTop;
   1067             var selectedTreeElement = this.treeOutline.selectedTreeElement;
   1068             if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
   1069                 this.select();
   1070             this.removeChildren();
   1071         }
   1072 
   1073         var treeElement = this;
   1074         var treeChildIndex = 0;
   1075         var elementToSelect;
   1076 
   1077         /**
   1078          * @this {WebInspector.ElementsTreeElement}
   1079          */
   1080         function updateChildrenOfNode()
   1081         {
   1082             var treeOutline = treeElement.treeOutline;
   1083             var visibleChildren = this._visibleChildren();
   1084 
   1085             for (var i = 0; i < visibleChildren.length; ++i) {
   1086                 var child = visibleChildren[i];
   1087                 var currentTreeElement = treeElement.children[treeChildIndex];
   1088                 if (!currentTreeElement || currentTreeElement._node !== child) {
   1089                     // Find any existing element that is later in the children list.
   1090                     var existingTreeElement = null;
   1091                     for (var j = (treeChildIndex + 1), size = treeElement.expandedChildCount; j < size; ++j) {
   1092                         if (treeElement.children[j]._node === child) {
   1093                             existingTreeElement = treeElement.children[j];
   1094                             break;
   1095                         }
   1096                     }
   1097 
   1098                     if (existingTreeElement && existingTreeElement.parent === treeElement) {
   1099                         // If an existing element was found and it has the same parent, just move it.
   1100                         treeElement.moveChild(existingTreeElement, treeChildIndex);
   1101                     } else {
   1102                         // No existing element found, insert a new element.
   1103                         if (treeChildIndex < treeElement.expandedChildrenLimit) {
   1104                             var newElement = treeElement.insertChildElement(child, treeChildIndex);
   1105                             if (child === selectedNode)
   1106                                 elementToSelect = newElement;
   1107                             if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit)
   1108                                 treeElement.expandedChildrenLimit++;
   1109                         }
   1110                     }
   1111                 }
   1112 
   1113                 ++treeChildIndex;
   1114             }
   1115         }
   1116 
   1117         // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
   1118         for (var i = (this.children.length - 1); i >= 0; --i) {
   1119             var currentChild = this.children[i];
   1120             var currentNode = currentChild._node;
   1121             if (!currentNode)
   1122                 continue;
   1123             var currentParentNode = currentNode.parentNode;
   1124 
   1125             if (currentParentNode === this._node)
   1126                 continue;
   1127 
   1128             var selectedTreeElement = this.treeOutline.selectedTreeElement;
   1129             if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
   1130                 this.select();
   1131 
   1132             this.removeChildAtIndex(i);
   1133         }
   1134 
   1135         updateChildrenOfNode.call(this);
   1136         this._adjustCollapsedRange();
   1137 
   1138         var lastChild = this.children[this.children.length - 1];
   1139         if (this._node.nodeType() == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
   1140             this.insertChildElement(this._node, this.children.length, true);
   1141 
   1142         // We want to restore the original selection and tree scroll position after a full refresh, if possible.
   1143         if (fullRefresh && elementToSelect) {
   1144             elementToSelect.select();
   1145             if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
   1146                 treeOutlineContainerElement.scrollTop = originalScrollTop;
   1147         }
   1148 
   1149         delete this._updateChildrenInProgress;
   1150     },
   1151 
   1152     _adjustCollapsedRange: function()
   1153     {
   1154         var visibleChildren = this._visibleChildren();
   1155         // Ensure precondition: only the tree elements for node children are found in the tree
   1156         // (not the Expand All button or the closing tag).
   1157         if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
   1158             this.removeChild(this.expandAllButtonElement.__treeElement);
   1159 
   1160         const childNodeCount = visibleChildren.length;
   1161 
   1162         // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
   1163         for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
   1164             this.insertChildElement(visibleChildren[i], i);
   1165 
   1166         const expandedChildCount = this.expandedChildCount;
   1167         if (childNodeCount > this.expandedChildCount) {
   1168             var targetButtonIndex = expandedChildCount;
   1169             if (!this.expandAllButtonElement) {
   1170                 var button = document.createElement("button");
   1171                 button.className = "show-all-nodes";
   1172                 button.value = "";
   1173                 var item = new TreeElement(button, null, false);
   1174                 item.selectable = false;
   1175                 item.expandAllButton = true;
   1176                 this.insertChild(item, targetButtonIndex);
   1177                 this.expandAllButtonElement = item.listItemElement.firstChild;
   1178                 this.expandAllButtonElement.__treeElement = item;
   1179                 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
   1180             } else if (!this.expandAllButtonElement.__treeElement.parent)
   1181                 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
   1182             this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount);
   1183         } else if (this.expandAllButtonElement)
   1184             delete this.expandAllButtonElement;
   1185     },
   1186 
   1187     handleLoadAllChildren: function()
   1188     {
   1189         this.expandedChildrenLimit = Math.max(this._visibleChildCount(), this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit);
   1190     },
   1191 
   1192     expandRecursively: function()
   1193     {
   1194         /**
   1195          * @this {WebInspector.ElementsTreeElement}
   1196          */
   1197         function callback()
   1198         {
   1199             TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE);
   1200         }
   1201 
   1202         this._node.getSubtree(-1, callback.bind(this));
   1203     },
   1204 
   1205     /**
   1206      * @override
   1207      */
   1208     onexpand: function()
   1209     {
   1210         if (this._elementCloseTag)
   1211             return;
   1212 
   1213         this.updateTitle();
   1214         this.treeOutline.updateSelection();
   1215     },
   1216 
   1217     oncollapse: function()
   1218     {
   1219         if (this._elementCloseTag)
   1220             return;
   1221 
   1222         this.updateTitle();
   1223         this.treeOutline.updateSelection();
   1224     },
   1225 
   1226     /**
   1227      * @override
   1228      */
   1229     onreveal: function()
   1230     {
   1231         if (this.listItemElement) {
   1232             var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name");
   1233             if (tagSpans.length)
   1234                 tagSpans[0].scrollIntoViewIfNeeded(true);
   1235             else
   1236                 this.listItemElement.scrollIntoViewIfNeeded(true);
   1237         }
   1238     },
   1239 
   1240     /**
   1241      * @override
   1242      */
   1243     onselect: function(selectedByUser)
   1244     {
   1245         this.treeOutline.suppressRevealAndSelect = true;
   1246         this.treeOutline.selectDOMNode(this._node, selectedByUser);
   1247         if (selectedByUser)
   1248             WebInspector.domAgent.highlightDOMNode(this._node.id);
   1249         this.updateSelection();
   1250         this.treeOutline.suppressRevealAndSelect = false;
   1251         return true;
   1252     },
   1253 
   1254     /**
   1255      * @override
   1256      */
   1257     ondelete: function()
   1258     {
   1259         var startTagTreeElement = this.treeOutline.findTreeElement(this._node);
   1260         startTagTreeElement ? startTagTreeElement.remove() : this.remove();
   1261         return true;
   1262     },
   1263 
   1264     /**
   1265      * @override
   1266      */
   1267     onenter: function()
   1268     {
   1269         // On Enter or Return start editing the first attribute
   1270         // or create a new attribute on the selected element.
   1271         if (this._editing)
   1272             return false;
   1273 
   1274         this._startEditing();
   1275 
   1276         // prevent a newline from being immediately inserted
   1277         return true;
   1278     },
   1279 
   1280     selectOnMouseDown: function(event)
   1281     {
   1282         TreeElement.prototype.selectOnMouseDown.call(this, event);
   1283 
   1284         if (this._editing)
   1285             return;
   1286 
   1287         if (this.treeOutline._showInElementsPanelEnabled) {
   1288             WebInspector.showPanel("elements");
   1289             this.treeOutline.selectDOMNode(this._node, true);
   1290         }
   1291 
   1292         // Prevent selecting the nearest word on double click.
   1293         if (event.detail >= 2)
   1294             event.preventDefault();
   1295     },
   1296 
   1297     /**
   1298      * @override
   1299      */
   1300     ondblclick: function(event)
   1301     {
   1302         if (this._editing || this._elementCloseTag)
   1303             return false;
   1304 
   1305         if (this._startEditingTarget(event.target))
   1306             return false;
   1307 
   1308         if (this.hasChildren && !this.expanded)
   1309             this.expand();
   1310         return false;
   1311     },
   1312 
   1313     _insertInLastAttributePosition: function(tag, node)
   1314     {
   1315         if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
   1316             tag.insertBefore(node, tag.lastChild);
   1317         else {
   1318             var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
   1319             tag.textContent = '';
   1320             tag.appendChild(document.createTextNode('<'+nodeName));
   1321             tag.appendChild(node);
   1322             tag.appendChild(document.createTextNode('>'));
   1323         }
   1324 
   1325         this.updateSelection();
   1326     },
   1327 
   1328     _startEditingTarget: function(eventTarget)
   1329     {
   1330         if (this.treeOutline.selectedDOMNode() != this._node)
   1331             return;
   1332 
   1333         if (this._node.nodeType() != Node.ELEMENT_NODE && this._node.nodeType() != Node.TEXT_NODE)
   1334             return false;
   1335 
   1336         var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node");
   1337         if (textNode)
   1338             return this._startEditingTextNode(textNode);
   1339 
   1340         var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute");
   1341         if (attribute)
   1342             return this._startEditingAttribute(attribute, eventTarget);
   1343 
   1344         var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
   1345         if (tagName)
   1346             return this._startEditingTagName(tagName);
   1347 
   1348         var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
   1349         if (newAttribute)
   1350             return this._addNewAttribute();
   1351 
   1352         return false;
   1353     },
   1354 
   1355     /**
   1356      * @param {!WebInspector.ContextMenu} contextMenu
   1357      * @param {?Event} event
   1358      */
   1359     _populateTagContextMenu: function(contextMenu, event)
   1360     {
   1361         // Add attribute-related actions.
   1362         var treeElement = this._elementCloseTag ? this.treeOutline.findTreeElement(this._node) : this;
   1363         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add attribute" : "Add Attribute"), this._addNewAttribute.bind(treeElement));
   1364 
   1365         var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
   1366         var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
   1367         if (attribute && !newAttribute)
   1368             contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit attribute" : "Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
   1369         contextMenu.appendSeparator();
   1370         if (this.treeOutline._setPseudoClassCallback) {
   1371             var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Force element state" : "Force Element State"));
   1372             this._populateForcedPseudoStateItems(pseudoSubMenu);
   1373             contextMenu.appendSeparator();
   1374         }
   1375         this._populateNodeContextMenu(contextMenu);
   1376         this.treeOutline._populateContextMenu(contextMenu, this._node);
   1377         this._populateScrollIntoView(contextMenu);
   1378     },
   1379 
   1380     /**
   1381      * @param {!WebInspector.ContextMenu} contextMenu
   1382      */
   1383     _populateScrollIntoView: function(contextMenu)
   1384     {
   1385         contextMenu.appendSeparator();
   1386         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Scroll into view" : "Scroll into View"), this._scrollIntoView.bind(this));
   1387     },
   1388 
   1389     _populateForcedPseudoStateItems: function(subMenu)
   1390     {
   1391         const pseudoClasses = ["active", "hover", "focus", "visited"];
   1392         var node = this._node;
   1393         var forcedPseudoState = (node ? node.getUserProperty("pseudoState") : null) || [];
   1394         for (var i = 0; i < pseudoClasses.length; ++i) {
   1395             var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0;
   1396             subMenu.appendCheckboxItem(":" + pseudoClasses[i], this.treeOutline._setPseudoClassCallback.bind(null, node.id, pseudoClasses[i], !pseudoClassForced), pseudoClassForced, false);
   1397         }
   1398     },
   1399 
   1400     _populateTextContextMenu: function(contextMenu, textNode)
   1401     {
   1402         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit text" : "Edit Text"), this._startEditingTextNode.bind(this, textNode));
   1403         this._populateNodeContextMenu(contextMenu);
   1404     },
   1405 
   1406     _populateNodeContextMenu: function(contextMenu)
   1407     {
   1408         // Add free-form node-related actions.
   1409         var openTagElement = this.treeOutline.getCachedTreeElement(this.representedObject) || this;
   1410         contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), openTagElement._editAsHTML.bind(openTagElement));
   1411         contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
   1412 
   1413         // Place it here so that all "Copy"-ing items stick together.
   1414         if (this.representedObject.nodeType() === Node.ELEMENT_NODE)
   1415             contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy CSS path" : "Copy CSS Path"), this._copyCSSPath.bind(this));
   1416         contextMenu.appendItem(WebInspector.UIString("Copy XPath"), this._copyXPath.bind(this));
   1417         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete node" : "Delete Node"), this.remove.bind(this));
   1418         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Inspect DOM properties" : "Inspect DOM Properties"), this._inspectDOMProperties.bind(this));
   1419     },
   1420 
   1421     _startEditing: function()
   1422     {
   1423         if (this.treeOutline.selectedDOMNode() !== this._node)
   1424             return;
   1425 
   1426         var listItem = this._listItemNode;
   1427 
   1428         if (this._canAddAttributes) {
   1429             var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
   1430             if (attribute)
   1431                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]);
   1432 
   1433             return this._addNewAttribute();
   1434         }
   1435 
   1436         if (this._node.nodeType() === Node.TEXT_NODE) {
   1437             var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0];
   1438             if (textNode)
   1439                 return this._startEditingTextNode(textNode);
   1440             return;
   1441         }
   1442     },
   1443 
   1444     _addNewAttribute: function()
   1445     {
   1446         // Cannot just convert the textual html into an element without
   1447         // a parent node. Use a temporary span container for the HTML.
   1448         var container = document.createElement("span");
   1449         this._buildAttributeDOM(container, " ", "");
   1450         var attr = container.firstChild;
   1451         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
   1452         attr.style.marginRight = "2px"; // overrides the .editing margin rule
   1453 
   1454         var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0];
   1455         this._insertInLastAttributePosition(tag, attr);
   1456         attr.scrollIntoViewIfNeeded(true);
   1457         return this._startEditingAttribute(attr, attr);
   1458     },
   1459 
   1460     _triggerEditAttribute: function(attributeName)
   1461     {
   1462         var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name");
   1463         for (var i = 0, len = attributeElements.length; i < len; ++i) {
   1464             if (attributeElements[i].textContent === attributeName) {
   1465                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
   1466                     if (elem.nodeType !== Node.ELEMENT_NODE)
   1467                         continue;
   1468 
   1469                     if (elem.classList.contains("webkit-html-attribute-value"))
   1470                         return this._startEditingAttribute(elem.parentNode, elem);
   1471                 }
   1472             }
   1473         }
   1474     },
   1475 
   1476     _startEditingAttribute: function(attribute, elementForSelection)
   1477     {
   1478         if (WebInspector.isBeingEdited(attribute))
   1479             return true;
   1480 
   1481         var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
   1482         if (!attributeNameElement)
   1483             return false;
   1484 
   1485         var attributeName = attributeNameElement.textContent;
   1486         var attributeValueElement = attribute.getElementsByClassName("webkit-html-attribute-value")[0];
   1487 
   1488         function removeZeroWidthSpaceRecursive(node)
   1489         {
   1490             if (node.nodeType === Node.TEXT_NODE) {
   1491                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
   1492                 return;
   1493             }
   1494 
   1495             if (node.nodeType !== Node.ELEMENT_NODE)
   1496                 return;
   1497 
   1498             for (var child = node.firstChild; child; child = child.nextSibling)
   1499                 removeZeroWidthSpaceRecursive(child);
   1500         }
   1501 
   1502         var domNode;
   1503         var listItemElement = attribute.enclosingNodeOrSelfWithNodeName("li");
   1504         if (attributeName && attributeValueElement && listItemElement && listItemElement.treeElement)
   1505             domNode = listItemElement.treeElement.representedObject;
   1506         var attributeValue = domNode ? domNode.getAttribute(attributeName) : undefined;
   1507         if (typeof attributeValue !== "undefined")
   1508             attributeValueElement.textContent = attributeValue;
   1509 
   1510         // Remove zero-width spaces that were added by nodeTitleInfo.
   1511         removeZeroWidthSpaceRecursive(attribute);
   1512 
   1513         var config = new WebInspector.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
   1514 
   1515         function handleKeyDownEvents(event)
   1516         {
   1517             var isMetaOrCtrl = WebInspector.isMac() ?
   1518                 event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
   1519                 event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
   1520             if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !config.multiline || isMetaOrCtrl))
   1521                 return "commit";
   1522             else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
   1523                 return "cancel";
   1524             else if (event.keyIdentifier === "U+0009") // Tab key
   1525                 return "move-" + (event.shiftKey ? "backward" : "forward");
   1526             else {
   1527                 WebInspector.handleElementValueModifications(event, attribute);
   1528                 return "";
   1529             }
   1530         }
   1531 
   1532         config.customFinishHandler = handleKeyDownEvents.bind(this);
   1533 
   1534         this._editing = WebInspector.startEditing(attribute, config);
   1535 
   1536         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
   1537 
   1538         return true;
   1539     },
   1540 
   1541     /**
   1542      * @param {!Element} textNodeElement
   1543      */
   1544     _startEditingTextNode: function(textNodeElement)
   1545     {
   1546         if (WebInspector.isBeingEdited(textNodeElement))
   1547             return true;
   1548 
   1549         var textNode = this._node;
   1550         // We only show text nodes inline in elements if the element only
   1551         // has a single child, and that child is a text node.
   1552         if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild)
   1553             textNode = textNode.firstChild;
   1554 
   1555         var container = textNodeElement.enclosingNodeOrSelfWithClass("webkit-html-text-node");
   1556         if (container)
   1557             container.textContent = textNode.nodeValue(); // Strip the CSS or JS highlighting if present.
   1558         var config = new WebInspector.EditingConfig(this._textNodeEditingCommitted.bind(this, textNode), this._editingCancelled.bind(this));
   1559         this._editing = WebInspector.startEditing(textNodeElement, config);
   1560         window.getSelection().setBaseAndExtent(textNodeElement, 0, textNodeElement, 1);
   1561 
   1562         return true;
   1563     },
   1564 
   1565     /**
   1566      * @param {!Element=} tagNameElement
   1567      */
   1568     _startEditingTagName: function(tagNameElement)
   1569     {
   1570         if (!tagNameElement) {
   1571             tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0];
   1572             if (!tagNameElement)
   1573                 return false;
   1574         }
   1575 
   1576         var tagName = tagNameElement.textContent;
   1577         if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()])
   1578             return false;
   1579 
   1580         if (WebInspector.isBeingEdited(tagNameElement))
   1581             return true;
   1582 
   1583         var closingTagElement = this._distinctClosingTagElement();
   1584 
   1585         /**
   1586          * @param {?Event} event
   1587          * @this {WebInspector.ElementsTreeElement}
   1588          */
   1589         function keyupListener(event)
   1590         {
   1591             if (closingTagElement)
   1592                 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
   1593         }
   1594 
   1595         /**
   1596          * @param {!Element} element
   1597          * @param {string} newTagName
   1598          * @this {WebInspector.ElementsTreeElement}
   1599          */
   1600         function editingComitted(element, newTagName)
   1601         {
   1602             tagNameElement.removeEventListener('keyup', keyupListener, false);
   1603             this._tagNameEditingCommitted.apply(this, arguments);
   1604         }
   1605 
   1606         /**
   1607          * @this {WebInspector.ElementsTreeElement}
   1608          */
   1609         function editingCancelled()
   1610         {
   1611             tagNameElement.removeEventListener('keyup', keyupListener, false);
   1612             this._editingCancelled.apply(this, arguments);
   1613         }
   1614 
   1615         tagNameElement.addEventListener('keyup', keyupListener, false);
   1616 
   1617         var config = new WebInspector.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName);
   1618         this._editing = WebInspector.startEditing(tagNameElement, config);
   1619         window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
   1620         return true;
   1621     },
   1622 
   1623     _startEditingAsHTML: function(commitCallback, error, initialValue)
   1624     {
   1625         if (error)
   1626             return;
   1627         if (this._editing)
   1628             return;
   1629 
   1630         function consume(event)
   1631         {
   1632             if (event.eventPhase === Event.AT_TARGET)
   1633                 event.consume(true);
   1634         }
   1635 
   1636         initialValue = this._convertWhitespaceToEntities(initialValue).text;
   1637 
   1638         this._htmlEditElement = document.createElement("div");
   1639         this._htmlEditElement.className = "source-code elements-tree-editor";
   1640 
   1641         // Hide header items.
   1642         var child = this.listItemElement.firstChild;
   1643         while (child) {
   1644             child.style.display = "none";
   1645             child = child.nextSibling;
   1646         }
   1647         // Hide children item.
   1648         if (this._childrenListNode)
   1649             this._childrenListNode.style.display = "none";
   1650         // Append editor.
   1651         this.listItemElement.appendChild(this._htmlEditElement);
   1652         this.treeOutline.childrenListElement.parentElement.addEventListener("mousedown", consume, false);
   1653 
   1654         this.updateSelection();
   1655 
   1656         /**
   1657          * @param {!Element} element
   1658          * @param {string} newValue
   1659          * @this {WebInspector.ElementsTreeElement}
   1660          */
   1661         function commit(element, newValue)
   1662         {
   1663             commitCallback(initialValue, newValue);
   1664             dispose.call(this);
   1665         }
   1666 
   1667         /**
   1668          * @this {WebInspector.ElementsTreeElement}
   1669          */
   1670         function dispose()
   1671         {
   1672             delete this._editing;
   1673             delete this.treeOutline._multilineEditing;
   1674 
   1675             // Remove editor.
   1676             this.listItemElement.removeChild(this._htmlEditElement);
   1677             delete this._htmlEditElement;
   1678             // Unhide children item.
   1679             if (this._childrenListNode)
   1680                 this._childrenListNode.style.removeProperty("display");
   1681             // Unhide header items.
   1682             var child = this.listItemElement.firstChild;
   1683             while (child) {
   1684                 child.style.removeProperty("display");
   1685                 child = child.nextSibling;
   1686             }
   1687 
   1688             this.treeOutline.childrenListElement.parentElement.removeEventListener("mousedown", consume, false);
   1689             this.updateSelection();
   1690             this.treeOutline.element.focus();
   1691         }
   1692 
   1693         var config = new WebInspector.EditingConfig(commit.bind(this), dispose.bind(this));
   1694         config.setMultilineOptions(initialValue, { name: "xml", htmlMode: true }, "web-inspector-html", WebInspector.settings.domWordWrap.get(), true);
   1695         this._editing = WebInspector.startEditing(this._htmlEditElement, config);
   1696         this._editing.setWidth(this.treeOutline._visibleWidth);
   1697         this.treeOutline._multilineEditing = this._editing;
   1698     },
   1699 
   1700     _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
   1701     {
   1702         delete this._editing;
   1703 
   1704         var treeOutline = this.treeOutline;
   1705 
   1706         /**
   1707          * @param {?Protocol.Error=} error
   1708          * @this {WebInspector.ElementsTreeElement}
   1709          */
   1710         function moveToNextAttributeIfNeeded(error)
   1711         {
   1712             if (error)
   1713                 this._editingCancelled(element, attributeName);
   1714 
   1715             if (!moveDirection)
   1716                 return;
   1717 
   1718             treeOutline._updateModifiedNodes();
   1719 
   1720             // Search for the attribute's position, and then decide where to move to.
   1721             var attributes = this._node.attributes();
   1722             for (var i = 0; i < attributes.length; ++i) {
   1723                 if (attributes[i].name !== attributeName)
   1724                     continue;
   1725 
   1726                 if (moveDirection === "backward") {
   1727                     if (i === 0)
   1728                         this._startEditingTagName();
   1729                     else
   1730                         this._triggerEditAttribute(attributes[i - 1].name);
   1731                 } else {
   1732                     if (i === attributes.length - 1)
   1733                         this._addNewAttribute();
   1734                     else
   1735                         this._triggerEditAttribute(attributes[i + 1].name);
   1736                 }
   1737                 return;
   1738             }
   1739 
   1740             // Moving From the "New Attribute" position.
   1741             if (moveDirection === "backward") {
   1742                 if (newText === " ") {
   1743                     // Moving from "New Attribute" that was not edited
   1744                     if (attributes.length > 0)
   1745                         this._triggerEditAttribute(attributes[attributes.length - 1].name);
   1746                 } else {
   1747                     // Moving from "New Attribute" that holds new value
   1748                     if (attributes.length > 1)
   1749                         this._triggerEditAttribute(attributes[attributes.length - 2].name);
   1750                 }
   1751             } else if (moveDirection === "forward") {
   1752                 if (!/^\s*$/.test(newText))
   1753                     this._addNewAttribute();
   1754                 else
   1755                     this._startEditingTagName();
   1756             }
   1757         }
   1758 
   1759         if (!attributeName.trim() && !newText.trim()) {
   1760             element.remove();
   1761             moveToNextAttributeIfNeeded.call(this);
   1762             return;
   1763         }
   1764 
   1765         if (oldText !== newText) {
   1766             this._node.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
   1767             return;
   1768         }
   1769 
   1770         this.updateTitle();
   1771         moveToNextAttributeIfNeeded.call(this);
   1772     },
   1773 
   1774     _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
   1775     {
   1776         delete this._editing;
   1777         var self = this;
   1778 
   1779         function cancel()
   1780         {
   1781             var closingTagElement = self._distinctClosingTagElement();
   1782             if (closingTagElement)
   1783                 closingTagElement.textContent = "</" + tagName + ">";
   1784 
   1785             self._editingCancelled(element, tagName);
   1786             moveToNextAttributeIfNeeded.call(self);
   1787         }
   1788 
   1789         /**
   1790          * @this {WebInspector.ElementsTreeElement}
   1791          */
   1792         function moveToNextAttributeIfNeeded()
   1793         {
   1794             if (moveDirection !== "forward") {
   1795                 this._addNewAttribute();
   1796                 return;
   1797             }
   1798 
   1799             var attributes = this._node.attributes();
   1800             if (attributes.length > 0)
   1801                 this._triggerEditAttribute(attributes[0].name);
   1802             else
   1803                 this._addNewAttribute();
   1804         }
   1805 
   1806         newText = newText.trim();
   1807         if (newText === oldText) {
   1808             cancel();
   1809             return;
   1810         }
   1811 
   1812         var treeOutline = this.treeOutline;
   1813         var wasExpanded = this.expanded;
   1814 
   1815         function changeTagNameCallback(error, nodeId)
   1816         {
   1817             if (error || !nodeId) {
   1818                 cancel();
   1819                 return;
   1820             }
   1821             var newTreeItem = treeOutline._selectNodeAfterEdit(wasExpanded, error, nodeId);
   1822             moveToNextAttributeIfNeeded.call(newTreeItem);
   1823         }
   1824 
   1825         this._node.setNodeName(newText, changeTagNameCallback);
   1826     },
   1827 
   1828     /**
   1829      * @param {!WebInspector.DOMNode} textNode
   1830      * @param {!Element} element
   1831      * @param {string} newText
   1832      */
   1833     _textNodeEditingCommitted: function(textNode, element, newText)
   1834     {
   1835         delete this._editing;
   1836 
   1837         /**
   1838          * @this {WebInspector.ElementsTreeElement}
   1839          */
   1840         function callback()
   1841         {
   1842             this.updateTitle();
   1843         }
   1844         textNode.setNodeValue(newText, callback.bind(this));
   1845     },
   1846 
   1847     /**
   1848      * @param {!Element} element
   1849      * @param {*} context
   1850      */
   1851     _editingCancelled: function(element, context)
   1852     {
   1853         delete this._editing;
   1854 
   1855         // Need to restore attributes structure.
   1856         this.updateTitle();
   1857     },
   1858 
   1859     /**
   1860      * @return {!Element}
   1861      */
   1862     _distinctClosingTagElement: function()
   1863     {
   1864         // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
   1865 
   1866         // For an expanded element, it will be the last element with class "close"
   1867         // in the child element list.
   1868         if (this.expanded) {
   1869             var closers = this._childrenListNode.querySelectorAll(".close");
   1870             return closers[closers.length-1];
   1871         }
   1872 
   1873         // Remaining cases are single line non-expanded elements with a closing
   1874         // tag, or HTML elements without a closing tag (such as <br>). Return
   1875         // null in the case where there isn't a closing tag.
   1876         var tags = this.listItemElement.getElementsByClassName("webkit-html-tag");
   1877         return (tags.length === 1 ? null : tags[tags.length-1]);
   1878     },
   1879 
   1880     /**
   1881      * @param {boolean=} onlySearchQueryChanged
   1882      */
   1883     updateTitle: function(onlySearchQueryChanged)
   1884     {
   1885         // If we are editing, return early to prevent canceling the edit.
   1886         // After editing is committed updateTitle will be called.
   1887         if (this._editing)
   1888             return;
   1889 
   1890         if (onlySearchQueryChanged) {
   1891             if (this._highlightResult)
   1892                 this._updateSearchHighlight(false);
   1893         } else {
   1894             var nodeInfo = this._nodeTitleInfo(WebInspector.linkifyURLAsNode);
   1895             if (nodeInfo.shadowRoot)
   1896                 this.listItemElement.classList.add("shadow-root");
   1897             var highlightElement = document.createElement("span");
   1898             highlightElement.className = "highlight";
   1899             highlightElement.appendChild(nodeInfo.titleDOM);
   1900             this.title = highlightElement;
   1901             this._updateDecorations();
   1902             delete this._highlightResult;
   1903         }
   1904 
   1905         delete this.selectionElement;
   1906         if (this.selected)
   1907             this.updateSelection();
   1908         this._preventFollowingLinksOnDoubleClick();
   1909         this._highlightSearchResults();
   1910     },
   1911 
   1912     /**
   1913      * @return {?Element}
   1914      */
   1915     _createDecoratorElement: function()
   1916     {
   1917         var node = this._node;
   1918         var decoratorMessages = [];
   1919         var parentDecoratorMessages = [];
   1920         for (var i = 0; i < this.treeOutline._nodeDecorators.length; ++i) {
   1921             var decorator = this.treeOutline._nodeDecorators[i];
   1922             var message = decorator.decorate(node);
   1923             if (message) {
   1924                 decoratorMessages.push(message);
   1925                 continue;
   1926             }
   1927 
   1928             if (this.expanded || this._elementCloseTag)
   1929                 continue;
   1930 
   1931             message = decorator.decorateAncestor(node);
   1932             if (message)
   1933                 parentDecoratorMessages.push(message)
   1934         }
   1935         if (!decoratorMessages.length && !parentDecoratorMessages.length)
   1936             return null;
   1937 
   1938         var decoratorElement = document.createElement("div");
   1939         decoratorElement.classList.add("elements-gutter-decoration");
   1940         if (!decoratorMessages.length)
   1941             decoratorElement.classList.add("elements-has-decorated-children");
   1942         decoratorElement.title = decoratorMessages.concat(parentDecoratorMessages).join("\n");
   1943         return decoratorElement;
   1944     },
   1945 
   1946     _updateDecorations: function()
   1947     {
   1948         if (this._decoratorElement)
   1949             this._decoratorElement.remove();
   1950         this._decoratorElement = this._createDecoratorElement();
   1951         if (this._decoratorElement && this.listItemElement)
   1952             this.listItemElement.insertBefore(this._decoratorElement, this.listItemElement.firstChild);
   1953     },
   1954 
   1955     /**
   1956      * @param {!Node} parentElement
   1957      * @param {string} name
   1958      * @param {string} value
   1959      * @param {!WebInspector.DOMNode=} node
   1960      * @param {function(string, string, string, boolean=, string=)=} linkify
   1961      */
   1962     _buildAttributeDOM: function(parentElement, name, value, node, linkify)
   1963     {
   1964         var hasText = (value.length > 0);
   1965         var attrSpanElement = parentElement.createChild("span", "webkit-html-attribute");
   1966         var attrNameElement = attrSpanElement.createChild("span", "webkit-html-attribute-name");
   1967         attrNameElement.textContent = name;
   1968 
   1969         if (hasText)
   1970             attrSpanElement.appendChild(document.createTextNode("=\u200B\""));
   1971 
   1972         if (linkify && (name === "src" || name === "href")) {
   1973             var rewrittenHref = node.resolveURL(value);
   1974             value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B");
   1975             if (rewrittenHref === null) {
   1976                 var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value");
   1977                 attrValueElement.textContent = value;
   1978             } else {
   1979                 if (value.startsWith("data:"))
   1980                     value = value.trimMiddle(60);
   1981                 attrSpanElement.appendChild(linkify(rewrittenHref, value, "webkit-html-attribute-value", node.nodeName().toLowerCase() === "a"));
   1982             }
   1983         } else {
   1984             value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B");
   1985             var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value");
   1986             attrValueElement.textContent = value;
   1987         }
   1988 
   1989         if (hasText)
   1990             attrSpanElement.appendChild(document.createTextNode("\""));
   1991     },
   1992 
   1993     /**
   1994      * @param {!Node} parentElement
   1995      * @param {string} pseudoElementName
   1996      */
   1997     _buildPseudoElementDOM: function(parentElement, pseudoElementName)
   1998     {
   1999         var pseudoElement = parentElement.createChild("span", "webkit-html-pseudo-element");
   2000         pseudoElement.textContent = "::" + pseudoElementName;
   2001         parentElement.appendChild(document.createTextNode("\u200B"));
   2002     },
   2003 
   2004     /**
   2005      * @param {!Node} parentElement
   2006      * @param {string} tagName
   2007      * @param {boolean} isClosingTag
   2008      * @param {boolean} isDistinctTreeElement
   2009      * @param {function(string, string, string, boolean=, string=)=} linkify
   2010      */
   2011     _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement, linkify)
   2012     {
   2013         var node = this._node;
   2014         var classes = [ "webkit-html-tag" ];
   2015         if (isClosingTag && isDistinctTreeElement)
   2016             classes.push("close");
   2017         var tagElement = parentElement.createChild("span", classes.join(" "));
   2018         tagElement.appendChild(document.createTextNode("<"));
   2019         var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "webkit-html-tag-name");
   2020         tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
   2021         if (!isClosingTag && node.hasAttributes()) {
   2022             var attributes = node.attributes();
   2023             for (var i = 0; i < attributes.length; ++i) {
   2024                 var attr = attributes[i];
   2025                 tagElement.appendChild(document.createTextNode(" "));
   2026                 this._buildAttributeDOM(tagElement, attr.name, attr.value, node, linkify);
   2027             }
   2028         }
   2029         tagElement.appendChild(document.createTextNode(">"));
   2030         parentElement.appendChild(document.createTextNode("\u200B"));
   2031     },
   2032 
   2033     /**
   2034      * @param {string} text
   2035      * @return {!{text: string, entityRanges: !Array.<!WebInspector.SourceRange>}}
   2036      */
   2037     _convertWhitespaceToEntities: function(text)
   2038     {
   2039         var result = "";
   2040         var resultLength = 0;
   2041         var lastIndexAfterEntity = 0;
   2042         var entityRanges = [];
   2043         var charToEntity = WebInspector.ElementsTreeOutline.MappedCharToEntity;
   2044         for (var i = 0, size = text.length; i < size; ++i) {
   2045             var char = text.charAt(i);
   2046             if (charToEntity[char]) {
   2047                 result += text.substring(lastIndexAfterEntity, i);
   2048                 var entityValue = "&" + charToEntity[char] + ";";
   2049                 entityRanges.push({offset: result.length, length: entityValue.length});
   2050                 result += entityValue;
   2051                 lastIndexAfterEntity = i + 1;
   2052             }
   2053         }
   2054         if (result)
   2055             result += text.substring(lastIndexAfterEntity);
   2056         return {text: result || text, entityRanges: entityRanges};
   2057     },
   2058 
   2059     /**
   2060      * @param {function(string, string, string, boolean=, string=)=} linkify
   2061      */
   2062     _nodeTitleInfo: function(linkify)
   2063     {
   2064         var node = this._node;
   2065         var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
   2066 
   2067         switch (node.nodeType()) {
   2068             case Node.ATTRIBUTE_NODE:
   2069                 var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
   2070                 this._buildAttributeDOM(info.titleDOM, node.name, value);
   2071                 break;
   2072 
   2073             case Node.ELEMENT_NODE:
   2074                 if (node.pseudoType()) {
   2075                     this._buildPseudoElementDOM(info.titleDOM, node.pseudoType());
   2076                     info.hasChildren = false;
   2077                     break;
   2078                 }
   2079 
   2080                 var tagName = node.nodeNameInCorrectCase();
   2081                 if (this._elementCloseTag) {
   2082                     this._buildTagDOM(info.titleDOM, tagName, true, true);
   2083                     info.hasChildren = false;
   2084                     break;
   2085                 }
   2086 
   2087                 this._buildTagDOM(info.titleDOM, tagName, false, false, linkify);
   2088 
   2089                 var showInlineText = this._showInlineText() && !this.hasChildren;
   2090                 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName]))) {
   2091                     if (this.hasChildren) {
   2092                         var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node bogus");
   2093                         textNodeElement.textContent = "\u2026";
   2094                         info.titleDOM.appendChild(document.createTextNode("\u200B"));
   2095                     }
   2096                     this._buildTagDOM(info.titleDOM, tagName, true, false);
   2097                 }
   2098 
   2099                 // If this element only has a single child that is a text node,
   2100                 // just show that text and the closing tag inline rather than
   2101                 // create a subtree for them
   2102                 if (showInlineText) {
   2103                     var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
   2104                     var result = this._convertWhitespaceToEntities(node.firstChild.nodeValue());
   2105                     textNodeElement.textContent = result.text;
   2106                     WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value");
   2107                     info.titleDOM.appendChild(document.createTextNode("\u200B"));
   2108                     this._buildTagDOM(info.titleDOM, tagName, true, false);
   2109                     info.hasChildren = false;
   2110                 }
   2111                 break;
   2112 
   2113             case Node.TEXT_NODE:
   2114                 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
   2115                     var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-js-node");
   2116                     newNode.textContent = node.nodeValue();
   2117 
   2118                     var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript", true);
   2119                     javascriptSyntaxHighlighter.syntaxHighlightNode(newNode);
   2120                 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
   2121                     var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-css-node");
   2122                     newNode.textContent = node.nodeValue();
   2123 
   2124                     var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css", true);
   2125                     cssSyntaxHighlighter.syntaxHighlightNode(newNode);
   2126                 } else {
   2127                     info.titleDOM.appendChild(document.createTextNode("\""));
   2128                     var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
   2129                     var result = this._convertWhitespaceToEntities(node.nodeValue());
   2130                     textNodeElement.textContent = result.text;
   2131                     WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value");
   2132                     info.titleDOM.appendChild(document.createTextNode("\""));
   2133                 }
   2134                 break;
   2135 
   2136             case Node.COMMENT_NODE:
   2137                 var commentElement = info.titleDOM.createChild("span", "webkit-html-comment");
   2138                 commentElement.appendChild(document.createTextNode("<!--" + node.nodeValue() + "-->"));
   2139                 break;
   2140 
   2141             case Node.DOCUMENT_TYPE_NODE:
   2142                 var docTypeElement = info.titleDOM.createChild("span", "webkit-html-doctype");
   2143                 docTypeElement.appendChild(document.createTextNode("<!DOCTYPE " + node.nodeName()));
   2144                 if (node.publicId) {
   2145                     docTypeElement.appendChild(document.createTextNode(" PUBLIC \"" + node.publicId + "\""));
   2146                     if (node.systemId)
   2147                         docTypeElement.appendChild(document.createTextNode(" \"" + node.systemId + "\""));
   2148                 } else if (node.systemId)
   2149                     docTypeElement.appendChild(document.createTextNode(" SYSTEM \"" + node.systemId + "\""));
   2150 
   2151                 if (node.internalSubset)
   2152                     docTypeElement.appendChild(document.createTextNode(" [" + node.internalSubset + "]"));
   2153 
   2154                 docTypeElement.appendChild(document.createTextNode(">"));
   2155                 break;
   2156 
   2157             case Node.CDATA_SECTION_NODE:
   2158                 var cdataElement = info.titleDOM.createChild("span", "webkit-html-text-node");
   2159                 cdataElement.appendChild(document.createTextNode("<![CDATA[" + node.nodeValue() + "]]>"));
   2160                 break;
   2161             case Node.DOCUMENT_FRAGMENT_NODE:
   2162                 var fragmentElement = info.titleDOM.createChild("span", "webkit-html-fragment");
   2163                 var nodeTitle;
   2164                 if (node.isInShadowTree()) {
   2165                     var shadowRootType = node.shadowRootType();
   2166                     if (shadowRootType) {
   2167                         info.shadowRoot = true;
   2168                         fragmentElement.classList.add("shadow-root");
   2169                         nodeTitle = "#shadow-root";
   2170                         if (shadowRootType === WebInspector.DOMNode.ShadowRootTypes.UserAgent)
   2171                             nodeTitle += " (" + shadowRootType + ")";
   2172                     }
   2173                 }
   2174                 if (!nodeTitle)
   2175                     nodeTitle = node.nodeNameInCorrectCase().collapseWhitespace();
   2176                 fragmentElement.textContent = nodeTitle;
   2177                 break;
   2178             default:
   2179                 info.titleDOM.appendChild(document.createTextNode(node.nodeNameInCorrectCase().collapseWhitespace()));
   2180         }
   2181         return info;
   2182     },
   2183 
   2184     /**
   2185      * @return {boolean}
   2186      */
   2187     _showInlineText: function()
   2188     {
   2189         if (this._node.templateContent() || (WebInspector.ElementsTreeOutline.showShadowDOM() && this._node.hasShadowRoots()) || this._node.hasPseudoElements())
   2190             return false;
   2191         if (this._node.nodeType() !== Node.ELEMENT_NODE)
   2192             return false;
   2193         if (!this._node.firstChild || this._node.firstChild !== this._node.lastChild || this._node.firstChild.nodeType() !== Node.TEXT_NODE)
   2194             return false;
   2195         var textChild = this._node.firstChild;
   2196         if (textChild.nodeValue().length < Preferences.maxInlineTextChildLength)
   2197             return true;
   2198         return false;
   2199     },
   2200 
   2201     remove: function()
   2202     {
   2203         if (this._node.pseudoType())
   2204             return;
   2205         var parentElement = this.parent;
   2206         if (!parentElement)
   2207             return;
   2208 
   2209         var self = this;
   2210         function removeNodeCallback(error, removedNodeId)
   2211         {
   2212             if (error)
   2213                 return;
   2214 
   2215             parentElement.removeChild(self);
   2216             parentElement._adjustCollapsedRange();
   2217         }
   2218 
   2219         if (!this._node.parentNode || this._node.parentNode.nodeType() === Node.DOCUMENT_NODE)
   2220             return;
   2221         this._node.removeNode(removeNodeCallback);
   2222     },
   2223 
   2224     _editAsHTML: function()
   2225     {
   2226         var node = this._node;
   2227         if (node.pseudoType())
   2228             return;
   2229 
   2230         var treeOutline = this.treeOutline;
   2231         var parentNode = node.parentNode;
   2232         var index = node.index;
   2233         var wasExpanded = this.expanded;
   2234 
   2235         function selectNode(error, nodeId)
   2236         {
   2237             if (error)
   2238                 return;
   2239 
   2240             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
   2241             treeOutline._updateModifiedNodes();
   2242 
   2243             var newNode = parentNode ? parentNode.children()[index] || parentNode : null;
   2244             if (!newNode)
   2245                 return;
   2246 
   2247             treeOutline.selectDOMNode(newNode, true);
   2248 
   2249             if (wasExpanded) {
   2250                 var newTreeItem = treeOutline.findTreeElement(newNode);
   2251                 if (newTreeItem)
   2252                     newTreeItem.expand();
   2253             }
   2254         }
   2255 
   2256         function commitChange(initialValue, value)
   2257         {
   2258             if (initialValue !== value)
   2259                 node.setOuterHTML(value, selectNode);
   2260             else
   2261                 return;
   2262         }
   2263 
   2264         node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
   2265     },
   2266 
   2267     _copyHTML: function()
   2268     {
   2269         this._node.copyNode();
   2270     },
   2271 
   2272     _copyCSSPath: function()
   2273     {
   2274         InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.cssPath(this._node, true));
   2275     },
   2276 
   2277     _copyXPath: function()
   2278     {
   2279         InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.xPath(this._node, true));
   2280     },
   2281 
   2282     _inspectDOMProperties: function()
   2283     {
   2284         WebInspector.RemoteObject.resolveNode(this._node, "console", callback);
   2285 
   2286         /**
   2287          * @param {?WebInspector.RemoteObject} nodeObject
   2288          */
   2289         function callback(nodeObject)
   2290         {
   2291             if (!nodeObject)
   2292                 return;
   2293 
   2294             var message = WebInspector.ConsoleMessage.create(WebInspector.ConsoleMessage.MessageSource.ConsoleAPI, WebInspector.ConsoleMessage.MessageLevel.Log, "", WebInspector.ConsoleMessage.MessageType.Dir, undefined, undefined, undefined, undefined, [nodeObject]);
   2295             WebInspector.console.addMessage(message);
   2296             WebInspector.showConsole();
   2297         }
   2298     },
   2299 
   2300     _highlightSearchResults: function()
   2301     {
   2302         if (!this._searchQuery || !this._searchHighlightsVisible)
   2303             return;
   2304         if (this._highlightResult) {
   2305             this._updateSearchHighlight(true);
   2306             return;
   2307         }
   2308 
   2309         var text = this.listItemElement.textContent;
   2310         var regexObject = createPlainTextSearchRegex(this._searchQuery, "gi");
   2311 
   2312         var offset = 0;
   2313         var match = regexObject.exec(text);
   2314         var matchRanges = [];
   2315         while (match) {
   2316             matchRanges.push(new WebInspector.SourceRange(match.index, match[0].length));
   2317             match = regexObject.exec(text);
   2318         }
   2319 
   2320         // Fall back for XPath, etc. matches.
   2321         if (!matchRanges.length)
   2322             matchRanges.push(new WebInspector.SourceRange(0, text.length));
   2323 
   2324         this._highlightResult = [];
   2325         WebInspector.highlightSearchResults(this.listItemElement, matchRanges, this._highlightResult);
   2326     },
   2327 
   2328     _scrollIntoView: function()
   2329     {
   2330         function scrollIntoViewCallback(object)
   2331         {
   2332             /**
   2333              * @this {!Element}
   2334              */
   2335             function scrollIntoView()
   2336             {
   2337                 this.scrollIntoViewIfNeeded(true);
   2338             }
   2339 
   2340             if (object)
   2341                 object.callFunction(scrollIntoView);
   2342         }
   2343 
   2344         WebInspector.RemoteObject.resolveNode(this._node, "", scrollIntoViewCallback);
   2345     },
   2346 
   2347     /**
   2348      * @return {!Array.<!WebInspector.DOMNode>} visibleChildren
   2349      */
   2350     _visibleChildren: function()
   2351     {
   2352         var visibleChildren = WebInspector.ElementsTreeOutline.showShadowDOM() ? this._node.shadowRoots() : [];
   2353         if (this._node.templateContent())
   2354             visibleChildren.push(this._node.templateContent());
   2355         var pseudoElements = this._node.pseudoElements();
   2356         if (pseudoElements[WebInspector.DOMNode.PseudoElementNames.Before])
   2357             visibleChildren.push(pseudoElements[WebInspector.DOMNode.PseudoElementNames.Before]);
   2358         if (this._node.childNodeCount())
   2359             visibleChildren = visibleChildren.concat(this._node.children());
   2360         if (pseudoElements[WebInspector.DOMNode.PseudoElementNames.After])
   2361             visibleChildren.push(pseudoElements[WebInspector.DOMNode.PseudoElementNames.After]);
   2362         return visibleChildren;
   2363     },
   2364 
   2365     /**
   2366      * @return {number}
   2367      */
   2368     _visibleChildCount: function()
   2369     {
   2370         var childCount = this._node.childNodeCount();
   2371         if (this._node.templateContent())
   2372             ++childCount;
   2373         if (WebInspector.ElementsTreeOutline.showShadowDOM())
   2374             childCount += this._node.shadowRoots().length;
   2375         for (var pseudoType in this._node.pseudoElements())
   2376             ++childCount;
   2377         return childCount;
   2378     },
   2379 
   2380     _updateHasChildren: function()
   2381     {
   2382         this.hasChildren = !this._elementCloseTag && !this._showInlineText() && this._visibleChildCount() > 0;
   2383     },
   2384 
   2385     __proto__: TreeElement.prototype
   2386 }
   2387 
   2388 /**
   2389  * @constructor
   2390  * @param {!WebInspector.ElementsTreeOutline} treeOutline
   2391  */
   2392 WebInspector.ElementsTreeUpdater = function(treeOutline)
   2393 {
   2394     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeInserted, this._nodeInserted, this);
   2395     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeRemoved, this._nodeRemoved, this);
   2396     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrModified, this._attributesUpdated, this);
   2397     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrRemoved, this._attributesUpdated, this);
   2398     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.CharacterDataModified, this._characterDataModified, this);
   2399     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.DocumentUpdated, this._documentUpdated, this);
   2400     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
   2401 
   2402     this._treeOutline = treeOutline;
   2403     /** @type {!Map.<!WebInspector.DOMNode, !WebInspector.ElementsTreeUpdater.UpdateEntry>} */
   2404     this._recentlyModifiedNodes = new Map();
   2405 }
   2406 
   2407 WebInspector.ElementsTreeUpdater.prototype = {
   2408     /**
   2409      * @param {!WebInspector.DOMNode} node
   2410      * @param {boolean} isUpdated
   2411      * @param {!WebInspector.DOMNode=} parentNode
   2412      */
   2413     _nodeModified: function(node, isUpdated, parentNode)
   2414     {
   2415         if (this._treeOutline._visible)
   2416             this._updateModifiedNodesSoon();
   2417 
   2418         var entry = this._recentlyModifiedNodes.get(node);
   2419         if (!entry) {
   2420             entry = new WebInspector.ElementsTreeUpdater.UpdateEntry(isUpdated, parentNode);
   2421             this._recentlyModifiedNodes.put(node, entry);
   2422             return;
   2423         }
   2424 
   2425         entry.isUpdated |= isUpdated;
   2426         if (parentNode)
   2427             entry.parent = parentNode;
   2428     },
   2429 
   2430     _documentUpdated: function(event)
   2431     {
   2432         var inspectedRootDocument = event.data;
   2433 
   2434         this._reset();
   2435 
   2436         if (!inspectedRootDocument)
   2437             return;
   2438 
   2439         this._treeOutline.rootDOMNode = inspectedRootDocument;
   2440     },
   2441 
   2442     _attributesUpdated: function(event)
   2443     {
   2444         this._nodeModified(event.data.node, true);
   2445     },
   2446 
   2447     _characterDataModified: function(event)
   2448     {
   2449         this._nodeModified(event.data, true);
   2450     },
   2451 
   2452     _nodeInserted: function(event)
   2453     {
   2454         this._nodeModified(event.data, false, event.data.parentNode);
   2455     },
   2456 
   2457     _nodeRemoved: function(event)
   2458     {
   2459         this._nodeModified(event.data.node, false, event.data.parent);
   2460     },
   2461 
   2462     _childNodeCountUpdated: function(event)
   2463     {
   2464         var treeElement = this._treeOutline.findTreeElement(event.data);
   2465         if (treeElement)
   2466             treeElement._updateHasChildren();
   2467     },
   2468 
   2469     _updateModifiedNodesSoon: function()
   2470     {
   2471         if (this._updateModifiedNodesTimeout)
   2472             return;
   2473         this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 50);
   2474     },
   2475 
   2476     _updateModifiedNodes: function()
   2477     {
   2478         if (this._updateModifiedNodesTimeout) {
   2479             clearTimeout(this._updateModifiedNodesTimeout);
   2480             delete this._updateModifiedNodesTimeout;
   2481         }
   2482 
   2483         var updatedParentTreeElements = [];
   2484 
   2485         var hidePanelWhileUpdating = this._recentlyModifiedNodes.size() > 10;
   2486         if (hidePanelWhileUpdating) {
   2487             var treeOutlineContainerElement = this._treeOutline.element.parentNode;
   2488             var originalScrollTop = treeOutlineContainerElement ? treeOutlineContainerElement.scrollTop : 0;
   2489             this._treeOutline.element.classList.add("hidden");
   2490         }
   2491 
   2492         var nodes = this._recentlyModifiedNodes.keys();
   2493         for (var i = 0, size = nodes.length; i < size; ++i) {
   2494             var node = nodes[i];
   2495             var entry = this._recentlyModifiedNodes.get(node);
   2496             var parent = entry.parent;
   2497 
   2498             if (parent === this._treeOutline._rootDOMNode) {
   2499                 // Document's children have changed, perform total update.
   2500                 this._treeOutline.update();
   2501                 this._treeOutline.element.classList.remove("hidden");
   2502                 return;
   2503             }
   2504 
   2505             if (entry.isUpdated) {
   2506                 var nodeItem = this._treeOutline.findTreeElement(node);
   2507                 if (nodeItem)
   2508                     nodeItem.updateTitle();
   2509             }
   2510 
   2511             var parentNodeItem = parent ? this._treeOutline.findTreeElement(parent) : null;
   2512             if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) {
   2513                 parentNodeItem.updateChildren();
   2514                 parentNodeItem.alreadyUpdatedChildren = true;
   2515                 updatedParentTreeElements.push(parentNodeItem);
   2516             }
   2517         }
   2518 
   2519         for (var i = 0; i < updatedParentTreeElements.length; ++i)
   2520             delete updatedParentTreeElements[i].alreadyUpdatedChildren;
   2521 
   2522         if (hidePanelWhileUpdating) {
   2523             this._treeOutline.element.classList.remove("hidden");
   2524             if (originalScrollTop)
   2525                 treeOutlineContainerElement.scrollTop = originalScrollTop;
   2526             this._treeOutline.updateSelection();
   2527         }
   2528         this._recentlyModifiedNodes.clear();
   2529 
   2530         this._treeOutline._fireElementsTreeUpdated(nodes);
   2531     },
   2532 
   2533     _reset: function()
   2534     {
   2535         this._treeOutline.rootDOMNode = null;
   2536         this._treeOutline.selectDOMNode(null, false);
   2537         WebInspector.domAgent.hideDOMNodeHighlight();
   2538         this._recentlyModifiedNodes.clear();
   2539     }
   2540 }
   2541 
   2542 /**
   2543  * @constructor
   2544  * @param {boolean} isUpdated
   2545  * @param {!WebInspector.DOMNode=} parent
   2546  */
   2547 WebInspector.ElementsTreeUpdater.UpdateEntry = function(isUpdated, parent)
   2548 {
   2549     this.isUpdated = isUpdated;
   2550     if (parent)
   2551         this.parent = parent;
   2552 }
   2553