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 */
39WebInspector.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}} */
75WebInspector.ElementsTreeOutline.ClipboardData;
76
77/**
78 * @enum {string}
79 */
80WebInspector.ElementsTreeOutline.Events = {
81    NodePicked: "NodePicked",
82    SelectedNodeChanged: "SelectedNodeChanged",
83    ElementsTreeUpdated: "ElementsTreeUpdated"
84}
85
86/**
87 * @const
88 * @type {!Object.<string, string>}
89 */
90WebInspector.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
108WebInspector.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 */
936WebInspector.ElementsTreeOutline.ElementDecorator = function()
937{
938}
939
940WebInspector.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 */
962WebInspector.ElementsTreeOutline.PseudoStateDecorator = function()
963{
964    WebInspector.ElementsTreeOutline.ElementDecorator.call(this);
965}
966
967WebInspector.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 */
1006WebInspector.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
1021WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
1022
1023// A union of HTML4 and HTML5-Draft elements that explicitly
1024// or implicitly (for HTML5) forbid the closing tag.
1025WebInspector.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.
1031WebInspector.ElementsTreeElement.EditTagBlacklist = [
1032    "html", "head", "body"
1033].keySet();
1034
1035WebInspector.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 */
2701WebInspector.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
2719WebInspector.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 */
2893WebInspector.ElementsTreeOutline.Renderer = function()
2894{
2895}
2896
2897WebInspector.ElementsTreeOutline.Renderer.prototype = {
2898    /**
2899     * @param {!Object} object
2900     * @return {?Element}
2901     */
2902    render: function(object)
2903    {
2904        if (!(object instanceof WebInspector.DOMNode))
2905            return null;
2906        var node = /** @type {!WebInspector.DOMNode} */ (object);
2907        var treeOutline = new WebInspector.ElementsTreeOutline(node.target(), false, false);
2908        treeOutline.rootDOMNode = node;
2909        treeOutline.element.classList.add("outline-disclosure");
2910        if (!treeOutline.children[0].hasChildren)
2911            treeOutline.element.classList.add("single-node");
2912        treeOutline.setVisible(true);
2913        treeOutline.element.treeElementForTest = treeOutline.children[0];
2914        return treeOutline.element;
2915    }
2916}
2917