806b203edb03f287131829ec98b3158eb115fba1
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / DOMTreeElement.js
1 /*
2  * Copyright (C) 2007, 2008, 2013 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 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 WebInspector.DOMTreeElement = function(node, elementCloseTag)
32 {
33     this._elementCloseTag = elementCloseTag;
34     var hasChildrenOverride = !elementCloseTag && node.hasChildNodes() && !this._showInlineText(node);
35
36     // The title will be updated in onattach.
37     TreeElement.call(this, "", node, hasChildrenOverride);
38
39     if (this.representedObject.nodeType() === Node.ELEMENT_NODE && !elementCloseTag)
40         this._canAddAttributes = true;
41     this._searchQuery = null;
42     this._expandedChildrenLimit = WebInspector.DOMTreeElement.InitialChildrenLimit;
43 };
44
45 WebInspector.DOMTreeElement.InitialChildrenLimit = 500;
46 WebInspector.DOMTreeElement.MaximumInlineTextChildLength = 80;
47
48 // A union of HTML4 and HTML5-Draft elements that explicitly
49 // or implicitly (for HTML5) forbid the closing tag.
50 // FIXME: Revise once HTML5 Final is published.
51 WebInspector.DOMTreeElement.ForbiddenClosingTagElements = [
52     "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
53     "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source"
54 ].keySet();
55
56 // These tags we do not allow editing their tag name.
57 WebInspector.DOMTreeElement.EditTagBlacklist = [
58     "html", "head", "body"
59 ].keySet();
60
61 WebInspector.DOMTreeElement.SearchHighlightStyleClassName = "search-highlight";
62 WebInspector.DOMTreeElement.BouncyHighlightStyleClassName = "bouncy-highlight";
63
64 WebInspector.DOMTreeElement.prototype = {
65     isCloseTag: function()
66     {
67         return this._elementCloseTag;
68     },
69
70     highlightSearchResults: function(searchQuery)
71     {
72         if (this._searchQuery !== searchQuery) {
73             this._updateSearchHighlight(false);
74             delete this._highlightResult; // A new search query.
75         }
76
77         this._searchQuery = searchQuery;
78         this._searchHighlightsVisible = true;
79         this.updateTitle(true);
80     },
81
82     hideSearchHighlights: function()
83     {
84         delete this._searchHighlightsVisible;
85         this._updateSearchHighlight(false);
86     },
87
88     emphasizeSearchHighlight: function()
89     {
90         var highlightElement = this.title.querySelector("." + WebInspector.DOMTreeElement.SearchHighlightStyleClassName);
91         console.assert(highlightElement);
92         if (!highlightElement)
93             return;
94
95         if (this._bouncyHighlightElement)
96             this._bouncyHighlightElement.remove();
97
98         this._bouncyHighlightElement = document.createElement("div");
99         this._bouncyHighlightElement.className = WebInspector.DOMTreeElement.BouncyHighlightStyleClassName;
100         this._bouncyHighlightElement.textContent = highlightElement.textContent;
101
102         // Position and show the bouncy highlight adjusting the coordinates to be inside the TreeOutline's space.
103         var highlightElementRect = highlightElement.getBoundingClientRect();
104         var treeOutlineRect = this.treeOutline.element.getBoundingClientRect();
105         this._bouncyHighlightElement.style.top = (highlightElementRect.top - treeOutlineRect.top) + "px";
106         this._bouncyHighlightElement.style.left = (highlightElementRect.left - treeOutlineRect.left) + "px";
107         this.title.appendChild(this._bouncyHighlightElement);
108
109         function animationEnded()
110         {
111             if (!this._bouncyHighlightElement)
112                 return;
113
114             this._bouncyHighlightElement.remove();
115             delete this._bouncyHighlightElement;
116         }
117
118         this._bouncyHighlightElement.addEventListener("webkitAnimationEnd", animationEnded.bind(this));
119     },
120
121     _updateSearchHighlight: function(show)
122     {
123         if (!this._highlightResult)
124             return;
125
126         function updateEntryShow(entry)
127         {
128             switch (entry.type) {
129                 case "added":
130                     entry.parent.insertBefore(entry.node, entry.nextSibling);
131                     break;
132                 case "changed":
133                     entry.node.textContent = entry.newText;
134                     break;
135             }
136         }
137
138         function updateEntryHide(entry)
139         {
140             switch (entry.type) {
141                 case "added":
142                     if (entry.node.parentElement)
143                         entry.node.parentElement.removeChild(entry.node);
144                     break;
145                 case "changed":
146                     entry.node.textContent = entry.oldText;
147                     break;
148             }
149         }
150
151         var updater = show ? updateEntryShow : updateEntryHide;
152
153         for (var i = 0, size = this._highlightResult.length; i < size; ++i)
154             updater(this._highlightResult[i]);
155     },
156
157     get hovered()
158     {
159         return this._hovered;
160     },
161
162     set hovered(x)
163     {
164         if (this._hovered === x)
165             return;
166
167         this._hovered = x;
168
169         if (this.listItemElement) {
170             if (x) {
171                 this.updateSelection();
172                 this.listItemElement.classList.add("hovered");
173             } else {
174                 this.listItemElement.classList.remove("hovered");
175             }
176         }
177     },
178
179     get expandedChildrenLimit()
180     {
181         return this._expandedChildrenLimit;
182     },
183
184     set expandedChildrenLimit(x)
185     {
186         if (this._expandedChildrenLimit === x)
187             return;
188
189         this._expandedChildrenLimit = x;
190         if (this.treeOutline && !this._updateChildrenInProgress)
191             this._updateChildren(true);
192     },
193
194     get expandedChildCount()
195     {
196         var count = this.children.length;
197         if (count && this.children[count - 1]._elementCloseTag)
198             count--;
199         if (count && this.children[count - 1].expandAllButton)
200             count--;
201         return count;
202     },
203
204     showChild: function(index)
205     {
206         console.assert(!this._elementCloseTag);
207         if (this._elementCloseTag)
208             return false;
209
210         if (index >= this.expandedChildrenLimit) {
211             this._expandedChildrenLimit = index + 1;
212             this._updateChildren(true);
213         }
214
215         // Whether index-th child is visible in the children tree
216         return this.expandedChildCount > index;
217     },
218
219     _createTooltipForNode: function()
220     {
221         var node = this.representedObject;
222         if (!node.nodeName() || node.nodeName().toLowerCase() !== "img")
223             return;
224
225         function setTooltip(error, result, wasThrown)
226         {
227             if (error || wasThrown || !result || result.type !== "string")
228                 return;
229
230             try {
231                 var properties = JSON.parse(result.description);
232                 var offsetWidth = properties[0];
233                 var offsetHeight = properties[1];
234                 var naturalWidth = properties[2];
235                 var naturalHeight = properties[3];
236                 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
237                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels").format(offsetWidth, offsetHeight);
238                 else
239                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)").format(offsetWidth, offsetHeight, naturalWidth, naturalHeight);
240             } catch (e) {
241                 console.error(e);
242             }
243         }
244
245         function resolvedNode(object)
246         {
247             if (!object)
248                 return;
249
250             function dimensions()
251             {
252                 return "[" + this.offsetWidth + "," + this.offsetHeight + "," + this.naturalWidth + "," + this.naturalHeight + "]";
253             }
254
255             object.callFunction(dimensions, undefined, false, setTooltip.bind(this));
256             object.release();
257         }
258         WebInspector.RemoteObject.resolveNode(node, "", resolvedNode.bind(this));
259     },
260
261     updateSelection: function()
262     {
263         var listItemElement = this.listItemElement;
264         if (!listItemElement)
265             return;
266
267         if (document.body.offsetWidth <= 0) {
268             // The stylesheet hasn't loaded yet or the window is closed,
269             // so we can't calculate what is need. Return early.
270             return;
271         }
272
273         if (!this.selectionElement) {
274             this.selectionElement = document.createElement("div");
275             this.selectionElement.className = "selection selected";
276             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
277         }
278
279         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
280     },
281
282     onattach: function()
283     {
284         if (this._hovered) {
285             this.updateSelection();
286             this.listItemElement.classList.add("hovered");
287         }
288
289         this.updateTitle();
290         this.listItemElement.draggable = true;
291         this.listItemElement.addEventListener("dragstart", this);
292     },
293
294     onpopulate: function()
295     {
296         if (this.children.length || this._showInlineText(this.representedObject) || this._elementCloseTag)
297             return;
298
299         this.updateChildren();
300     },
301
302     expandRecursively: function()
303     {
304         function callback()
305         {
306             TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE);
307         }
308
309         this.representedObject.getSubtree(-1, callback.bind(this));
310     },
311
312     updateChildren: function(fullRefresh)
313     {
314         if (this._elementCloseTag)
315             return;
316         this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh));
317     },
318
319     insertChildElement: function(child, index, closingTag)
320     {
321         var newElement = new WebInspector.DOMTreeElement(child, closingTag);
322         newElement.selectable = this.treeOutline._selectEnabled;
323         this.insertChild(newElement, index);
324         return newElement;
325     },
326
327     moveChild: function(child, targetIndex)
328     {
329         var wasSelected = child.selected;
330         this.removeChild(child);
331         this.insertChild(child, targetIndex);
332         if (wasSelected)
333             child.select();
334     },
335
336     _updateChildren: function(fullRefresh)
337     {
338         if (this._updateChildrenInProgress || !this.treeOutline._visible)
339             return;
340
341         this._updateChildrenInProgress = true;
342         var selectedNode = this.treeOutline.selectedDOMNode();
343         var originalScrollTop = 0;
344         if (fullRefresh) {
345             var treeOutlineContainerElement = this.treeOutline.element.parentNode;
346             originalScrollTop = treeOutlineContainerElement.scrollTop;
347             var selectedTreeElement = this.treeOutline.selectedTreeElement;
348             if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
349                 this.select();
350             this.removeChildren();
351         }
352
353         var treeElement = this;
354         var treeChildIndex = 0;
355         var elementToSelect;
356
357         function updateChildrenOfNode(node)
358         {
359             var treeOutline = treeElement.treeOutline;
360             var child = node.firstChild;
361             while (child) {
362                 var currentTreeElement = treeElement.children[treeChildIndex];
363                 if (!currentTreeElement || currentTreeElement.representedObject !== child) {
364                     // Find any existing element that is later in the children list.
365                     var existingTreeElement = null;
366                     for (var i = (treeChildIndex + 1), size = treeElement.expandedChildCount; i < size; ++i) {
367                         if (treeElement.children[i].representedObject === child) {
368                             existingTreeElement = treeElement.children[i];
369                             break;
370                         }
371                     }
372
373                     if (existingTreeElement && existingTreeElement.parent === treeElement) {
374                         // If an existing element was found and it has the same parent, just move it.
375                         treeElement.moveChild(existingTreeElement, treeChildIndex);
376                     } else {
377                         // No existing element found, insert a new element.
378                         if (treeChildIndex < treeElement.expandedChildrenLimit) {
379                             var newElement = treeElement.insertChildElement(child, treeChildIndex);
380                             if (child === selectedNode)
381                                 elementToSelect = newElement;
382                             if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit)
383                                 treeElement.expandedChildrenLimit++;
384                         }
385                     }
386                 }
387
388                 child = child.nextSibling;
389                 ++treeChildIndex;
390             }
391         }
392
393         // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
394         for (var i = (this.children.length - 1); i >= 0; --i) {
395             var currentChild = this.children[i];
396             var currentNode = currentChild.representedObject;
397             var currentParentNode = currentNode.parentNode;
398
399             if (currentParentNode === this.representedObject)
400                 continue;
401
402             var selectedTreeElement = this.treeOutline.selectedTreeElement;
403             if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
404                 this.select();
405
406             this.removeChildAtIndex(i);
407         }
408
409         updateChildrenOfNode(this.representedObject);
410         this.adjustCollapsedRange();
411
412         var lastChild = this.children.lastValue;
413         if (this.representedObject.nodeType() === Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
414             this.insertChildElement(this.representedObject, this.children.length, true);
415
416         // We want to restore the original selection and tree scroll position after a full refresh, if possible.
417         if (fullRefresh && elementToSelect) {
418             elementToSelect.select();
419             if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
420                 treeOutlineContainerElement.scrollTop = originalScrollTop;
421         }
422
423         delete this._updateChildrenInProgress;
424     },
425
426     adjustCollapsedRange: function()
427     {
428         // Ensure precondition: only the tree elements for node children are found in the tree
429         // (not the Expand All button or the closing tag).
430         if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
431             this.removeChild(this.expandAllButtonElement.__treeElement);
432
433         const node = this.representedObject;
434         if (!node.children)
435             return;
436         const childNodeCount = node.children.length;
437
438         // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
439         for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
440             this.insertChildElement(node.children[i], i);
441
442         const expandedChildCount = this.expandedChildCount;
443         if (childNodeCount > this.expandedChildCount) {
444             var targetButtonIndex = expandedChildCount;
445             if (!this.expandAllButtonElement) {
446                 var button = document.createElement("button");
447                 button.className = "show-all-nodes";
448                 button.value = "";
449
450                 var item = new TreeElement(button, null, false);
451                 item.selectable = false;
452                 item.expandAllButton = true;
453
454                 this.insertChild(item, targetButtonIndex);
455                 this.expandAllButtonElement = button;
456                 this.expandAllButtonElement.__treeElement = item;
457                 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
458             } else if (!this.expandAllButtonElement.__treeElement.parent)
459                 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
460
461             this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)").format(childNodeCount - expandedChildCount);
462         } else if (this.expandAllButtonElement)
463             delete this.expandAllButtonElement;
464     },
465
466     handleLoadAllChildren: function()
467     {
468         this.expandedChildrenLimit = Math.max(this.representedObject.childNodeCount, this.expandedChildrenLimit + WebInspector.DOMTreeElement.InitialChildrenLimit);
469     },
470
471     onexpand: function()
472     {
473         if (this._elementCloseTag)
474             return;
475
476         this.updateTitle();
477         this.treeOutline.updateSelection();
478     },
479
480     oncollapse: function()
481     {
482         if (this._elementCloseTag)
483             return;
484
485         this.updateTitle();
486         this.treeOutline.updateSelection();
487     },
488
489     onreveal: function()
490     {
491         if (this.listItemElement) {
492             var tagSpans = this.listItemElement.getElementsByClassName("html-tag-name");
493             if (tagSpans.length)
494                 tagSpans[0].scrollIntoViewIfNeeded(false);
495             else
496                 this.listItemElement.scrollIntoViewIfNeeded(false);
497         }
498     },
499
500     onselect: function(treeElement, selectedByUser)
501     {
502         this.treeOutline.suppressRevealAndSelect = true;
503         this.treeOutline.selectDOMNode(this.representedObject, selectedByUser);
504         if (selectedByUser)
505             WebInspector.domTreeManager.highlightDOMNode(this.representedObject.id);
506         this.updateSelection();
507         this.treeOutline.suppressRevealAndSelect = false;
508     },
509
510     ondeselect: function(treeElement)
511     {
512         this.treeOutline.selectDOMNode(null);
513     },
514
515     ondelete: function()
516     {
517         if (this.representedObject.isInShadowTree())
518             return false;
519
520         var startTagTreeElement = this.treeOutline.findTreeElement(this.representedObject);
521         if (startTagTreeElement)
522             startTagTreeElement.remove();
523         else
524             this.remove();
525         return true;
526     },
527
528     onenter: function()
529     {
530         // On Enter or Return start editing the first attribute
531         // or create a new attribute on the selected element.
532         if (this.treeOutline.editing)
533             return false;
534
535         this._startEditing();
536
537         // prevent a newline from being immediately inserted
538         return true;
539     },
540
541     selectOnMouseDown: function(event)
542     {
543         TreeElement.prototype.selectOnMouseDown.call(this, event);
544
545         if (this._editing)
546             return;
547
548         // Prevent selecting the nearest word on double click.
549         if (event.detail >= 2)
550             event.preventDefault();
551     },
552
553     ondblclick: function(event)
554     {
555         if (this._editing || this._elementCloseTag)
556             return;
557
558         if (this._startEditingTarget(event.target))
559             return;
560
561         if (this.hasChildren && !this.expanded)
562             this.expand();
563     },
564
565     _insertInLastAttributePosition: function(tag, node)
566     {
567         if (tag.getElementsByClassName("html-attribute").length > 0)
568             tag.insertBefore(node, tag.lastChild);
569         else {
570             var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
571             tag.textContent = "";
572             tag.appendChild(document.createTextNode("<" + nodeName));
573             tag.appendChild(node);
574             tag.appendChild(document.createTextNode(">"));
575         }
576
577         this.updateSelection();
578     },
579
580     _startEditingTarget: function(eventTarget)
581     {
582         if (this.treeOutline.selectedDOMNode() !== this.representedObject)
583             return false;
584
585         if (this.representedObject.isInShadowTree())
586             return false;
587
588         if (this.representedObject.nodeType() !== Node.ELEMENT_NODE && this.representedObject.nodeType() !== Node.TEXT_NODE)
589             return false;
590
591         var textNode = eventTarget.enclosingNodeOrSelfWithClass("html-text-node");
592         if (textNode)
593             return this._startEditingTextNode(textNode);
594
595         var attribute = eventTarget.enclosingNodeOrSelfWithClass("html-attribute");
596         if (attribute)
597             return this._startEditingAttribute(attribute, eventTarget);
598
599         var tagName = eventTarget.enclosingNodeOrSelfWithClass("html-tag-name");
600         if (tagName)
601             return this._startEditingTagName(tagName);
602
603         var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
604         if (newAttribute)
605             return this._addNewAttribute();
606
607         return false;
608     },
609
610     _populateTagContextMenu: function(contextMenu, event)
611     {
612         var node = this.representedObject;
613         if (!node.isInShadowTree()) {
614             var attribute = event.target.enclosingNodeOrSelfWithClass("html-attribute");
615             var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
616
617             // Add attribute-related actions.
618             contextMenu.appendItem(WebInspector.UIString("Add Attribute"), this._addNewAttribute.bind(this));
619             if (attribute && !newAttribute)
620                 contextMenu.appendItem(WebInspector.UIString("Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
621             contextMenu.appendSeparator();
622
623             if (WebInspector.cssStyleManager.canForcePseudoClasses()) {
624                 var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString("Forced Pseudo-Classes"));
625                 this._populateForcedPseudoStateItems(pseudoSubMenu);
626                 contextMenu.appendSeparator();
627             }
628         }
629
630         this._populateNodeContextMenu(contextMenu);
631         this.treeOutline._populateContextMenu(contextMenu, this.representedObject);
632     },
633
634     _populateForcedPseudoStateItems: function(subMenu)
635     {
636         var node = this.representedObject;
637         var enabledPseudoClasses = node.enabledPseudoClasses;
638         // These strings don't need to be localized as they are CSS pseudo-classes.
639         WebInspector.CSSStyleManager.ForceablePseudoClasses.forEach(function(pseudoClass) {
640             var label = pseudoClass.capitalize();
641             var enabled = enabledPseudoClasses.contains(pseudoClass);
642             subMenu.appendCheckboxItem(label, function() {
643                 node.setPseudoClassEnabled(pseudoClass, !enabled);
644             }, enabled, false);
645         });
646     },
647
648     _populateTextContextMenu: function(contextMenu, textNode)
649     {
650         var node = this.representedObject;
651         if (!node.isInShadowTree())
652             contextMenu.appendItem(WebInspector.UIString("Edit Text"), this._startEditingTextNode.bind(this, textNode));
653
654         this._populateNodeContextMenu(contextMenu);
655     },
656
657     _populateNodeContextMenu: function(contextMenu)
658     {
659         // Add free-form node-related actions.
660         var node = this.representedObject;
661         if (!node.isInShadowTree())
662             contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
663         contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
664         if (!node.isInShadowTree())
665             contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this));
666     },
667
668     _startEditing: function()
669     {
670         if (this.treeOutline.selectedDOMNode() !== this.representedObject)
671             return false;
672
673         if (this.representedObject.isInShadowTree())
674             return false;
675
676         var listItem = this._listItemNode;
677
678         if (this._canAddAttributes) {
679             var attribute = listItem.getElementsByClassName("html-attribute")[0];
680             if (attribute)
681                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("html-attribute-value")[0]);
682
683             return this._addNewAttribute();
684         }
685
686         if (this.representedObject.nodeType() === Node.TEXT_NODE) {
687             var textNode = listItem.getElementsByClassName("html-text-node")[0];
688             if (textNode)
689                 return this._startEditingTextNode(textNode);
690             return false;
691         }
692     },
693
694     _addNewAttribute: function()
695     {
696         // Cannot just convert the textual html into an element without
697         // a parent node. Use a temporary span container for the HTML.
698         var container = document.createElement("span");
699         this._buildAttributeDOM(container, " ", "");
700         var attr = container.firstChild;
701         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
702         attr.style.marginRight = "2px"; // overrides the .editing margin rule
703
704         var tag = this.listItemElement.getElementsByClassName("html-tag")[0];
705         this._insertInLastAttributePosition(tag, attr);
706         return this._startEditingAttribute(attr, attr);
707     },
708
709     _triggerEditAttribute: function(attributeName)
710     {
711         var attributeElements = this.listItemElement.getElementsByClassName("html-attribute-name");
712         for (var i = 0, len = attributeElements.length; i < len; ++i) {
713             if (attributeElements[i].textContent === attributeName) {
714                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
715                     if (elem.nodeType !== Node.ELEMENT_NODE)
716                         continue;
717
718                     if (elem.classList.contains("html-attribute-value"))
719                         return this._startEditingAttribute(elem.parentNode, elem);
720                 }
721             }
722         }
723     },
724
725     _startEditingAttribute: function(attribute, elementForSelection)
726     {
727         if (WebInspector.isBeingEdited(attribute))
728             return true;
729
730         var attributeNameElement = attribute.getElementsByClassName("html-attribute-name")[0];
731         if (!attributeNameElement)
732             return false;
733
734         var attributeName = attributeNameElement.textContent;
735
736         function removeZeroWidthSpaceRecursive(node)
737         {
738             if (node.nodeType === Node.TEXT_NODE) {
739                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
740                 return;
741             }
742
743             if (node.nodeType !== Node.ELEMENT_NODE)
744                 return;
745
746             for (var child = node.firstChild; child; child = child.nextSibling)
747                 removeZeroWidthSpaceRecursive(child);
748         }
749
750         // Remove zero-width spaces that were added by nodeTitleInfo.
751         removeZeroWidthSpaceRecursive(attribute);
752
753         var config = new WebInspector.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
754         this._editing = WebInspector.startEditing(attribute, config);
755
756         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
757
758         return true;
759     },
760
761     _startEditingTextNode: function(textNode)
762     {
763         if (WebInspector.isBeingEdited(textNode))
764             return true;
765
766         var config = new WebInspector.EditingConfig(this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
767         config.spellcheck = true;
768         this._editing = WebInspector.startEditing(textNode, config);
769         window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
770
771         return true;
772     },
773
774     _startEditingTagName: function(tagNameElement)
775     {
776         if (!tagNameElement) {
777             tagNameElement = this.listItemElement.getElementsByClassName("html-tag-name")[0];
778             if (!tagNameElement)
779                 return false;
780         }
781
782         var tagName = tagNameElement.textContent;
783         if (WebInspector.DOMTreeElement.EditTagBlacklist[tagName.toLowerCase()])
784             return false;
785
786         if (WebInspector.isBeingEdited(tagNameElement))
787             return true;
788
789         var closingTagElement = this._distinctClosingTagElement();
790
791         function keyupListener(event)
792         {
793             if (closingTagElement)
794                 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
795         }
796
797         function editingComitted(element, newTagName)
798         {
799             tagNameElement.removeEventListener("keyup", keyupListener, false);
800             this._tagNameEditingCommitted.apply(this, arguments);
801         }
802
803         function editingCancelled()
804         {
805             tagNameElement.removeEventListener("keyup", keyupListener, false);
806             this._editingCancelled.apply(this, arguments);
807         }
808
809         tagNameElement.addEventListener("keyup", keyupListener, false);
810
811         var config = new WebInspector.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName);
812         this._editing = WebInspector.startEditing(tagNameElement, config);
813         window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
814         return true;
815     },
816
817     _startEditingAsHTML: function(commitCallback, error, initialValue)
818     {
819         if (error)
820             return;
821         if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement))
822             return;
823
824         this._htmlEditElement = document.createElement("div");
825         this._htmlEditElement.className = "source-code elements-tree-editor";
826         this._htmlEditElement.textContent = initialValue;
827
828         // Hide header items.
829         var child = this.listItemElement.firstChild;
830         while (child) {
831             child.style.display = "none";
832             child = child.nextSibling;
833         }
834         // Hide children item.
835         if (this._childrenListNode)
836             this._childrenListNode.style.display = "none";
837         // Append editor.
838         this.listItemElement.appendChild(this._htmlEditElement);
839
840         this.updateSelection();
841
842         function commit()
843         {
844             commitCallback(this._htmlEditElement.textContent);
845             dispose.call(this);
846         }
847
848         function dispose()
849         {
850             this._editing = false;
851
852             // Remove editor.
853             this.listItemElement.removeChild(this._htmlEditElement);
854             delete this._htmlEditElement;
855             // Unhide children item.
856             if (this._childrenListNode)
857                 this._childrenListNode.style.removeProperty("display");
858             // Unhide header items.
859             var child = this.listItemElement.firstChild;
860             while (child) {
861                 child.style.removeProperty("display");
862                 child = child.nextSibling;
863             }
864
865             this.updateSelection();
866         }
867
868         var config = new WebInspector.EditingConfig(commit.bind(this), dispose.bind(this));
869         config.setMultiline(true);
870         this._editing = WebInspector.startEditing(this._htmlEditElement, config);
871     },
872
873     _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
874     {
875         this._editing = false;
876
877         var treeOutline = this.treeOutline;
878         function moveToNextAttributeIfNeeded(error)
879         {
880             if (error)
881                 this._editingCancelled(element, attributeName);
882
883             if (!moveDirection)
884                 return;
885
886             treeOutline._updateModifiedNodes();
887
888             // Search for the attribute's position, and then decide where to move to.
889             var attributes = this.representedObject.attributes();
890             for (var i = 0; i < attributes.length; ++i) {
891                 if (attributes[i].name !== attributeName)
892                     continue;
893
894                 if (moveDirection === "backward") {
895                     if (i === 0)
896                         this._startEditingTagName();
897                     else
898                         this._triggerEditAttribute(attributes[i - 1].name);
899                 } else {
900                     if (i === attributes.length - 1)
901                         this._addNewAttribute();
902                     else
903                         this._triggerEditAttribute(attributes[i + 1].name);
904                 }
905                 return;
906             }
907
908             // Moving From the "New Attribute" position.
909             if (moveDirection === "backward") {
910                 if (newText === " ") {
911                     // Moving from "New Attribute" that was not edited
912                     if (attributes.length)
913                         this._triggerEditAttribute(attributes.lastValue.name);
914                 } else {
915                     // Moving from "New Attribute" that holds new value
916                     if (attributes.length > 1)
917                         this._triggerEditAttribute(attributes[attributes.length - 2].name);
918                 }
919             } else if (moveDirection === "forward") {
920                 if (!/^\s*$/.test(newText))
921                     this._addNewAttribute();
922                 else
923                     this._startEditingTagName();
924             }
925         }
926
927         this.representedObject.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
928     },
929
930     _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
931     {
932         this._editing = false;
933         var self = this;
934
935         function cancel()
936         {
937             var closingTagElement = self._distinctClosingTagElement();
938             if (closingTagElement)
939                 closingTagElement.textContent = "</" + tagName + ">";
940
941             self._editingCancelled(element, tagName);
942             moveToNextAttributeIfNeeded.call(self);
943         }
944
945         function moveToNextAttributeIfNeeded()
946         {
947             if (moveDirection !== "forward") {
948                 this._addNewAttribute();
949                 return;
950             }
951
952             var attributes = this.representedObject.attributes();
953             if (attributes.length > 0)
954                 this._triggerEditAttribute(attributes[0].name);
955             else
956                 this._addNewAttribute();
957         }
958
959         newText = newText.trim();
960         if (newText === oldText) {
961             cancel();
962             return;
963         }
964
965         var treeOutline = this.treeOutline;
966         var wasExpanded = this.expanded;
967
968         function changeTagNameCallback(error, nodeId)
969         {
970             if (error || !nodeId) {
971                 cancel();
972                 return;
973             }
974
975             var node = WebInspector.domTreeManager.nodeForId(nodeId);
976
977             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
978             treeOutline._updateModifiedNodes();
979             treeOutline.selectDOMNode(node, true);
980
981             var newTreeItem = treeOutline.findTreeElement(node);
982             if (wasExpanded)
983                 newTreeItem.expand();
984
985             moveToNextAttributeIfNeeded.call(newTreeItem);
986         }
987
988         this.representedObject.setNodeName(newText, changeTagNameCallback);
989     },
990
991     _textNodeEditingCommitted: function(element, newText)
992     {
993         this._editing = false;
994
995         var textNode;
996         if (this.representedObject.nodeType() === Node.ELEMENT_NODE) {
997             // We only show text nodes inline in elements if the element only
998             // has a single child, and that child is a text node.
999             textNode = this.representedObject.firstChild;
1000         } else if (this.representedObject.nodeType() === Node.TEXT_NODE)
1001             textNode = this.representedObject;
1002
1003         textNode.setNodeValue(newText, this.updateTitle.bind(this));
1004     },
1005
1006     _editingCancelled: function(element, context)
1007     {
1008         this._editing = false;
1009
1010         // Need to restore attributes structure.
1011         this.updateTitle();
1012     },
1013
1014     _distinctClosingTagElement: function()
1015     {
1016         // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
1017
1018         // For an expanded element, it will be the last element with class "close"
1019         // in the child element list.
1020         if (this.expanded) {
1021             var closers = this._childrenListNode.querySelectorAll(".close");
1022             return closers[closers.length - 1];
1023         }
1024
1025         // Remaining cases are single line non-expanded elements with a closing
1026         // tag, or HTML elements without a closing tag (such as <br>). Return
1027         // null in the case where there isn't a closing tag.
1028         var tags = this.listItemElement.getElementsByClassName("html-tag");
1029         return (tags.length === 1 ? null : tags[tags.length - 1]);
1030     },
1031
1032     updateTitle: function(onlySearchQueryChanged)
1033     {
1034         // If we are editing, return early to prevent canceling the edit.
1035         // After editing is committed updateTitle will be called.
1036         if (this._editing)
1037             return;
1038
1039         if (onlySearchQueryChanged) {
1040             if (this._highlightResult)
1041                 this._updateSearchHighlight(false);
1042         } else {
1043             this.title = document.createElement("span");
1044             this.title.appendChild(this._nodeTitleInfo().titleDOM);
1045             delete this._highlightResult;
1046         }
1047
1048         delete this.selectionElement;
1049         this.updateSelection();
1050         this._highlightSearchResults();
1051     },
1052
1053     _buildAttributeDOM: function(parentElement, name, value, node)
1054     {
1055         var hasText = (value.length > 0);
1056         var attrSpanElement = parentElement.createChild("span", "html-attribute");
1057         var attrNameElement = attrSpanElement.createChild("span", "html-attribute-name");
1058         attrNameElement.textContent = name;
1059
1060         if (hasText)
1061             attrSpanElement.appendChild(document.createTextNode("=\u200B\""));
1062
1063         if (name === "src" || name === "href") {
1064             var baseURL = node.ownerDocument ? node.ownerDocument.documentURL : null;
1065             var rewrittenURL = absoluteURL(value, baseURL);
1066
1067             value = value.insertWordBreakCharacters();
1068
1069             if (!rewrittenURL) {
1070                 var attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1071                 attrValueElement.textContent = value;
1072             } else {
1073                 if (value.startsWith("data:"))
1074                     value = value.trimMiddle(60);
1075
1076                 var linkElement = document.createElement("a");
1077                 linkElement.href = rewrittenURL;
1078                 linkElement.textContent = value;
1079
1080                 attrSpanElement.appendChild(linkElement);
1081             }
1082         } else {
1083             value = value.insertWordBreakCharacters();
1084             var attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1085             attrValueElement.textContent = value;
1086         }
1087
1088         if (hasText)
1089             attrSpanElement.appendChild(document.createTextNode("\""));
1090     },
1091
1092     _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement)
1093     {
1094         var node = this.representedObject;
1095         var classes = [ "html-tag" ];
1096         if (isClosingTag && isDistinctTreeElement)
1097             classes.push("close");
1098         if (node.isInShadowTree())
1099             classes.push("shadow");
1100         var tagElement = parentElement.createChild("span", classes.join(" "));
1101         tagElement.appendChild(document.createTextNode("<"));
1102         var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "html-tag-name");
1103         tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
1104         if (!isClosingTag && node.hasAttributes()) {
1105             var attributes = node.attributes();
1106             for (var i = 0; i < attributes.length; ++i) {
1107                 var attr = attributes[i];
1108                 tagElement.appendChild(document.createTextNode(" "));
1109                 this._buildAttributeDOM(tagElement, attr.name, attr.value, node);
1110             }
1111         }
1112         tagElement.appendChild(document.createTextNode(">"));
1113         parentElement.appendChild(document.createTextNode("\u200B"));
1114     },
1115
1116     _nodeTitleInfo: function()
1117     {
1118         var node = this.representedObject;
1119         var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
1120
1121         switch (node.nodeType()) {
1122             case Node.DOCUMENT_FRAGMENT_NODE:
1123                 var fragmentElement = info.titleDOM.createChild("span", "webkit-html-fragment");
1124                 if (node.isInShadowTree()) {
1125                     fragmentElement.textContent = WebInspector.UIString("Shadow Content");
1126                     fragmentElement.classList.add("shadow");
1127                 } else
1128                     fragmentElement.textContent = WebInspector.UIString("Document Fragment");
1129                 break;
1130
1131             case Node.ATTRIBUTE_NODE:
1132                 var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
1133                 this._buildAttributeDOM(info.titleDOM, node.name, value);
1134                 break;
1135
1136             case Node.ELEMENT_NODE:
1137                 var tagName = node.nodeNameInCorrectCase();
1138                 if (this._elementCloseTag) {
1139                     this._buildTagDOM(info.titleDOM, tagName, true, true);
1140                     info.hasChildren = false;
1141                     break;
1142                 }
1143
1144                 this._buildTagDOM(info.titleDOM, tagName, false, false);
1145
1146                 var textChild = this._singleTextChild(node);
1147                 var showInlineText = textChild && textChild.nodeValue().length < WebInspector.DOMTreeElement.MaximumInlineTextChildLength;
1148
1149                 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.DOMTreeElement.ForbiddenClosingTagElements[tagName]))) {
1150                     if (this.hasChildren) {
1151                         var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1152                         textNodeElement.textContent = "\u2026";
1153                         info.titleDOM.appendChild(document.createTextNode("\u200B"));
1154                     }
1155                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1156                 }
1157
1158                 // If this element only has a single child that is a text node,
1159                 // just show that text and the closing tag inline rather than
1160                 // create a subtree for them
1161                 if (showInlineText) {
1162                     var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1163                     var nodeNameLowerCase = node.nodeName().toLowerCase();
1164
1165                     if (nodeNameLowerCase === "script")
1166                         textNodeElement.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/javascript"));
1167                     else if (nodeNameLowerCase === "style")
1168                         textNodeElement.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/css"));
1169                     else
1170                         textNodeElement.textContent = textChild.nodeValue();
1171
1172                     info.titleDOM.appendChild(document.createTextNode("\u200B"));
1173
1174                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1175                     info.hasChildren = false;
1176                 }
1177                 break;
1178
1179             case Node.TEXT_NODE:
1180                 function trimedNodeValue()
1181                 {
1182                     // Trim empty lines from the beginning and extra space at the end since most style and script tags begin with a newline
1183                     // and end with a newline and indentation for the end tag.
1184                     return node.nodeValue().replace(/^[\n\r]*/, "").replace(/\s*$/, "");
1185                 }
1186
1187                 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
1188                     var newNode = info.titleDOM.createChild("span", "html-text-node large");
1189                     newNode.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/javascript"));
1190                 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
1191                     var newNode = info.titleDOM.createChild("span", "html-text-node large");
1192                     newNode.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/css"));
1193                 } else {
1194                     info.titleDOM.appendChild(document.createTextNode("\""));
1195                     var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1196                     textNodeElement.textContent = node.nodeValue();
1197                     info.titleDOM.appendChild(document.createTextNode("\""));
1198                 }
1199                 break;
1200
1201             case Node.COMMENT_NODE:
1202                 var commentElement = info.titleDOM.createChild("span", "html-comment");
1203                 commentElement.appendChild(document.createTextNode("<!--" + node.nodeValue() + "-->"));
1204                 break;
1205
1206             case Node.DOCUMENT_TYPE_NODE:
1207                 var docTypeElement = info.titleDOM.createChild("span", "html-doctype");
1208                 docTypeElement.appendChild(document.createTextNode("<!DOCTYPE " + node.nodeName()));
1209                 if (node.publicId) {
1210                     docTypeElement.appendChild(document.createTextNode(" PUBLIC \"" + node.publicId + "\""));
1211                     if (node.systemId)
1212                         docTypeElement.appendChild(document.createTextNode(" \"" + node.systemId + "\""));
1213                 } else if (node.systemId)
1214                     docTypeElement.appendChild(document.createTextNode(" SYSTEM \"" + node.systemId + "\""));
1215
1216                 if (node.internalSubset)
1217                     docTypeElement.appendChild(document.createTextNode(" [" + node.internalSubset + "]"));
1218
1219                 docTypeElement.appendChild(document.createTextNode(">"));
1220                 break;
1221
1222             case Node.CDATA_SECTION_NODE:
1223                 var cdataElement = info.titleDOM.createChild("span", "html-text-node");
1224                 cdataElement.appendChild(document.createTextNode("<![CDATA[" + node.nodeValue() + "]]>"));
1225                 break;
1226
1227             case Node.PROCESSING_INSTRUCTION_NODE:
1228                 var processingInstructionElement = info.titleDOM.createChild("span", "html-processing-instruction");
1229                 var data = node.nodeValue();
1230                 var dataString = data.length ? " " + data : "";
1231                 var title = "<?" + node.nodeNameInCorrectCase() + dataString + "?>";
1232                 processingInstructionElement.appendChild(document.createTextNode(title));
1233                 break;
1234
1235             default:
1236                 var defaultElement = info.titleDOM.appendChild(document.createTextNode(node.nodeNameInCorrectCase().collapseWhitespace()));
1237         }
1238
1239         return info;
1240     },
1241
1242     _singleTextChild: function(node)
1243     {
1244         if (!node)
1245             return null;
1246
1247         var firstChild = node.firstChild;
1248         if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE)
1249             return null;
1250
1251         if (node.hasShadowRoots())
1252             return null;
1253
1254         var sibling = firstChild.nextSibling;
1255         return sibling ? null : firstChild;
1256     },
1257
1258     _showInlineText: function(node)
1259     {
1260         if (node.nodeType() === Node.ELEMENT_NODE) {
1261             var textChild = this._singleTextChild(node);
1262             if (textChild && textChild.nodeValue().length < WebInspector.DOMTreeElement.MaximumInlineTextChildLength)
1263                 return true;
1264         }
1265         return false;
1266     },
1267
1268     remove: function()
1269     {
1270         var parentElement = this.parent;
1271         if (!parentElement)
1272             return;
1273
1274         var self = this;
1275         function removeNodeCallback(error, removedNodeId)
1276         {
1277             if (error)
1278                 return;
1279
1280             if (!self.parent)
1281                 return;
1282
1283             parentElement.removeChild(self);
1284             parentElement.adjustCollapsedRange();
1285         }
1286
1287         this.representedObject.removeNode(removeNodeCallback);
1288     },
1289
1290     _editAsHTML: function()
1291     {
1292         var treeOutline = this.treeOutline;
1293         var node = this.representedObject;
1294         var parentNode = node.parentNode;
1295         var index = node.index;
1296         var wasExpanded = this.expanded;
1297
1298         function selectNode(error, nodeId)
1299         {
1300             if (error)
1301                 return;
1302
1303             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1304             treeOutline._updateModifiedNodes();
1305
1306             var newNode = parentNode ? parentNode.children[index] || parentNode : null;
1307             if (!newNode)
1308                 return;
1309
1310             treeOutline.selectDOMNode(newNode, true);
1311
1312             if (wasExpanded) {
1313                 var newTreeItem = treeOutline.findTreeElement(newNode);
1314                 if (newTreeItem)
1315                     newTreeItem.expand();
1316             }
1317         }
1318
1319         function commitChange(value)
1320         {
1321             node.setOuterHTML(value, selectNode);
1322         }
1323
1324         node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
1325     },
1326
1327     _copyHTML: function()
1328     {
1329         this.representedObject.copyNode();
1330     },
1331
1332     _highlightSearchResults: function()
1333     {
1334         if (!this.title || !this._searchQuery || !this._searchHighlightsVisible)
1335             return;
1336
1337         if (this._highlightResult) {
1338             this._updateSearchHighlight(true);
1339             return;
1340         }
1341
1342         var text = this.title.textContent;
1343         var searchRegex = new RegExp(this._searchQuery.escapeForRegExp(), "gi");
1344
1345         var offset = 0;
1346         var match = searchRegex.exec(text);
1347         var matchRanges = [];
1348         while (match) {
1349             matchRanges.push({ offset: match.index, length: match[0].length });
1350             match = searchRegex.exec(text);
1351         }
1352
1353         // Fall back for XPath, etc. matches.
1354         if (!matchRanges.length)
1355             matchRanges.push({ offset: 0, length: text.length });
1356
1357         this._highlightResult = [];
1358         WebInspector.highlightRangesWithStyleClass(this.title, matchRanges, WebInspector.DOMTreeElement.SearchHighlightStyleClassName, this._highlightResult);
1359     },
1360
1361     handleEvent: function(event)
1362     {
1363         if (event.type === "dragstart" && this._editing)
1364             event.preventDefault();
1365     }
1366 };
1367
1368 WebInspector.DOMTreeElement.prototype.__proto__ = TreeElement.prototype;