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