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