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