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