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