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