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