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