Web Inspector: Clicking DOM breakpoint marker should enable/disable breakpoints
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / DOMTreeElement.js
1 /*
2  * Copyright (C) 2007, 2008, 2013, 2015, 2016 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 = class DOMTreeElement extends WebInspector.TreeElement
32 {
33     constructor(node, elementCloseTag)
34     {
35         super("", node);
36
37         this._elementCloseTag = elementCloseTag;
38         this.hasChildren = !elementCloseTag && this._hasVisibleChildren();
39
40         if (this.representedObject.nodeType() === Node.ELEMENT_NODE && !elementCloseTag)
41             this._canAddAttributes = true;
42         this._searchQuery = null;
43         this._expandedChildrenLimit = WebInspector.DOMTreeElement.InitialChildrenLimit;
44         this._breakpointStatus = WebInspector.DOMTreeElement.BreakpointStatus.None;
45
46         this._recentlyModifiedAttributes = [];
47         this._boundNodeChangedAnimationEnd = this._nodeChangedAnimationEnd.bind(this);
48
49         node.addEventListener(WebInspector.DOMNode.Event.EnabledPseudoClassesChanged, this._nodePseudoClassesDidChange, this);
50     }
51
52     // Static
53
54     static shadowRootTypeDisplayName(type)
55     {
56         switch (type) {
57         case WebInspector.DOMNode.ShadowRootType.UserAgent:
58             return WebInspector.UIString("User Agent");
59         case WebInspector.DOMNode.ShadowRootType.Open:
60             return WebInspector.UIString("Open");
61         case WebInspector.DOMNode.ShadowRootType.Closed:
62             return WebInspector.UIString("Closed");
63         }
64     }
65
66     // Public
67
68     get breakpointStatus()
69     {
70         return this._breakpointStatus;
71     }
72
73     set breakpointStatus(status)
74     {
75         if (this._breakpointStatus === status)
76             return;
77
78         this._breakpointStatus = status;
79         this._updateBreakpointStatus();
80     }
81
82     isCloseTag()
83     {
84         return this._elementCloseTag;
85     }
86
87     highlightSearchResults(searchQuery)
88     {
89         if (this._searchQuery !== searchQuery) {
90             this._updateSearchHighlight(false);
91             this._highlightResult = undefined; // A new search query.
92         }
93
94         this._searchQuery = searchQuery;
95         this._searchHighlightsVisible = true;
96         this.updateTitle(true);
97     }
98
99     hideSearchHighlights()
100     {
101         this._searchHighlightsVisible = false;
102         this._updateSearchHighlight(false);
103     }
104
105     emphasizeSearchHighlight()
106     {
107         var highlightElement = this.title.querySelector("." + WebInspector.DOMTreeElement.SearchHighlightStyleClassName);
108         console.assert(highlightElement);
109         if (!highlightElement)
110             return;
111
112         if (this._bouncyHighlightElement)
113             this._bouncyHighlightElement.remove();
114
115         this._bouncyHighlightElement = document.createElement("div");
116         this._bouncyHighlightElement.className = WebInspector.DOMTreeElement.BouncyHighlightStyleClassName;
117         this._bouncyHighlightElement.textContent = highlightElement.textContent;
118
119         // Position and show the bouncy highlight adjusting the coordinates to be inside the TreeOutline's space.
120         var highlightElementRect = highlightElement.getBoundingClientRect();
121         var treeOutlineRect = this.treeOutline.element.getBoundingClientRect();
122         this._bouncyHighlightElement.style.top = (highlightElementRect.top - treeOutlineRect.top) + "px";
123         this._bouncyHighlightElement.style.left = (highlightElementRect.left - treeOutlineRect.left) + "px";
124         this.title.appendChild(this._bouncyHighlightElement);
125
126         function animationEnded()
127         {
128             if (!this._bouncyHighlightElement)
129                 return;
130
131             this._bouncyHighlightElement.remove();
132             this._bouncyHighlightElement = null;
133         }
134
135         this._bouncyHighlightElement.addEventListener("animationend", animationEnded.bind(this));
136     }
137
138     _updateSearchHighlight(show)
139     {
140         if (!this._highlightResult)
141             return;
142
143         function updateEntryShow(entry)
144         {
145             switch (entry.type) {
146                 case "added":
147                     entry.parent.insertBefore(entry.node, entry.nextSibling);
148                     break;
149                 case "changed":
150                     entry.node.textContent = entry.newText;
151                     break;
152             }
153         }
154
155         function updateEntryHide(entry)
156         {
157             switch (entry.type) {
158                 case "added":
159                     entry.node.remove();
160                     break;
161                 case "changed":
162                     entry.node.textContent = entry.oldText;
163                     break;
164             }
165         }
166
167         var updater = show ? updateEntryShow : updateEntryHide;
168
169         for (var i = 0, size = this._highlightResult.length; i < size; ++i)
170             updater(this._highlightResult[i]);
171     }
172
173     get hovered()
174     {
175         return this._hovered;
176     }
177
178     set hovered(value)
179     {
180         if (this._hovered === value)
181             return;
182
183         this._hovered = value;
184
185         if (this.listItemElement) {
186             this.listItemElement.classList.toggle("hovered", this._hovered);
187             this.updateSelectionArea();
188         }
189     }
190
191     get editable()
192     {
193         let node = this.representedObject;
194
195         if (node.isShadowRoot() || node.isInUserAgentShadowTree())
196             return false;
197
198         if (node.isPseudoElement())
199             return false;
200
201         return this.treeOutline.editable;
202     }
203
204     get expandedChildrenLimit()
205     {
206         return this._expandedChildrenLimit;
207     }
208
209     set expandedChildrenLimit(x)
210     {
211         if (this._expandedChildrenLimit === x)
212             return;
213
214         this._expandedChildrenLimit = x;
215         if (this.treeOutline && !this._updateChildrenInProgress)
216             this._updateChildren(true);
217     }
218
219     get expandedChildCount()
220     {
221         var count = this.children.length;
222         if (count && this.children[count - 1]._elementCloseTag)
223             count--;
224         if (count && this.children[count - 1].expandAllButton)
225             count--;
226         return count;
227     }
228
229     attributeDidChange(name)
230     {
231         this._recentlyModifiedAttributes.push({name});
232     }
233
234     showChildNode(node)
235     {
236         console.assert(!this._elementCloseTag);
237         if (this._elementCloseTag)
238             return null;
239
240         var index = this._visibleChildren().indexOf(node);
241         if (index === -1)
242             return null;
243
244         if (index >= this.expandedChildrenLimit) {
245             this._expandedChildrenLimit = index + 1;
246             this._updateChildren(true);
247         }
248
249         return this.children[index];
250     }
251
252     _createTooltipForNode()
253     {
254         var node = this.representedObject;
255         if (!node.nodeName() || node.nodeName().toLowerCase() !== "img")
256             return;
257
258         function setTooltip(error, result, wasThrown)
259         {
260             if (error || wasThrown || !result || result.type !== "string")
261                 return;
262
263             try {
264                 var properties = JSON.parse(result.description);
265                 var offsetWidth = properties[0];
266                 var offsetHeight = properties[1];
267                 var naturalWidth = properties[2];
268                 var naturalHeight = properties[3];
269                 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
270                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels").format(offsetWidth, offsetHeight);
271                 else
272                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)").format(offsetWidth, offsetHeight, naturalWidth, naturalHeight);
273             } catch (e) {
274                 console.error(e);
275             }
276         }
277
278         function resolvedNode(object)
279         {
280             if (!object)
281                 return;
282
283             function dimensions()
284             {
285                 return "[" + this.offsetWidth + "," + this.offsetHeight + "," + this.naturalWidth + "," + this.naturalHeight + "]";
286             }
287
288             object.callFunction(dimensions, undefined, false, setTooltip.bind(this));
289             object.release();
290         }
291         WebInspector.RemoteObject.resolveNode(node, "", resolvedNode.bind(this));
292     }
293
294     updateSelectionArea()
295     {
296         let listItemElement = this.listItemElement;
297         if (!listItemElement)
298             return;
299
300         // If there's no reason to have a selection area, remove the DOM element.
301         let indicatesTreeOutlineState = this.treeOutline && (this.treeOutline.dragOverTreeElement === this || this.treeOutline.selectedTreeElement === this);
302         if (!this.hovered && !this.pseudoClassesEnabled && !indicatesTreeOutlineState) {
303             if (this._selectionElement) {
304                 this._selectionElement.remove();
305                 this._selectionElement = null;
306             }
307
308             return;
309         }
310
311         if (!this._selectionElement) {
312             this._selectionElement = document.createElement("div");
313             this._selectionElement.className = "selection-area";
314             listItemElement.insertBefore(this._selectionElement, listItemElement.firstChild);
315         }
316
317         this._selectionElement.style.height = listItemElement.offsetHeight + "px";
318     }
319
320     onattach()
321     {
322         if (this.hovered)
323             this.listItemElement.classList.add("hovered");
324
325         this.updateTitle();
326
327         if (this.editable) {
328             this.listItemElement.draggable = true;
329             this.listItemElement.addEventListener("dragstart", this);
330         }
331     }
332
333     onpopulate()
334     {
335         if (this.children.length || !this._hasVisibleChildren() || this._elementCloseTag)
336             return;
337
338         this.updateChildren();
339     }
340
341     expandRecursively()
342     {
343         this.representedObject.getSubtree(-1, super.expandRecursively.bind(this, Number.MAX_VALUE));
344     }
345
346     updateChildren(fullRefresh)
347     {
348         if (this._elementCloseTag)
349             return;
350
351         this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh));
352     }
353
354     insertChildElement(child, index, closingTag)
355     {
356         var newElement = new WebInspector.DOMTreeElement(child, closingTag);
357         newElement.selectable = this.treeOutline._selectEnabled;
358         this.insertChild(newElement, index);
359         return newElement;
360     }
361
362     moveChild(child, targetIndex)
363     {
364         // No move needed if the child is already in the right place.
365         if (this.children[targetIndex] === child)
366             return;
367
368         var originalSelectedChild = this.treeOutline.selectedTreeElement;
369
370         this.removeChild(child);
371         this.insertChild(child, targetIndex);
372
373         if (originalSelectedChild !== this.treeOutline.selectedTreeElement)
374             originalSelectedChild.select();
375     }
376
377     _updateChildren(fullRefresh)
378     {
379         if (this._updateChildrenInProgress || !this.treeOutline._visible)
380             return;
381
382         this._updateChildrenInProgress = true;
383
384         var node = this.representedObject;
385         var selectedNode = this.treeOutline.selectedDOMNode();
386         var originalScrollTop = 0;
387
388         var hasVisibleChildren = this._hasVisibleChildren();
389
390         if (fullRefresh || !hasVisibleChildren) {
391             var treeOutlineContainerElement = this.treeOutline.element.parentNode;
392             originalScrollTop = treeOutlineContainerElement.scrollTop;
393             var selectedTreeElement = this.treeOutline.selectedTreeElement;
394             if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
395                 this.select();
396             this.removeChildren();
397
398             // No longer have children.
399             if (!hasVisibleChildren) {
400                 this.hasChildren = false;
401                 this.updateTitle();
402                 this._updateChildrenInProgress = false;
403                 return;
404             }
405         }
406
407         // We now have children.
408         if (!this.hasChildren) {
409             this.hasChildren = true;
410             this.updateTitle();
411         }
412
413         // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
414         // Keep a list of existing tree elements for nodes that we can use later.
415         var existingChildTreeElements = new Map;
416         for (var i = this.children.length - 1; i >= 0; --i) {
417             var currentChildTreeElement = this.children[i];
418             var currentNode = currentChildTreeElement.representedObject;
419             var currentParentNode = currentNode.parentNode;
420             if (currentParentNode === node) {
421                 existingChildTreeElements.set(currentNode, currentChildTreeElement);
422                 continue;
423             }
424
425             this.removeChildAtIndex(i);
426         }
427
428         // Move / create TreeElements for our visible children.
429         var elementToSelect = null;
430         var visibleChildren = this._visibleChildren();
431         for (var i = 0; i < visibleChildren.length && i < this.expandedChildrenLimit; ++i) {
432             var childNode = visibleChildren[i];
433
434             // Already have a tree element for this child, just move it.
435             var existingChildTreeElement = existingChildTreeElements.get(childNode);
436             if (existingChildTreeElement) {
437                 this.moveChild(existingChildTreeElement, i);
438                 continue;
439             }
440
441             // No existing tree element for this child. Insert a new element.
442             var newChildTreeElement = this.insertChildElement(childNode, i);
443
444             // Update state.
445             if (childNode === selectedNode)
446                 elementToSelect = newChildTreeElement;
447             if (this.expandedChildCount > this.expandedChildrenLimit)
448                 this.expandedChildrenLimit++;
449         }
450
451         // Update expand all children button.
452         this.adjustCollapsedRange();
453
454         // Insert closing tag tree element.
455         var lastChild = this.children.lastValue;
456         if (node.nodeType() === Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
457             this.insertChildElement(this.representedObject, this.children.length, true);
458
459         // We want to restore the original selection and tree scroll position after a full refresh, if possible.
460         if (fullRefresh && elementToSelect) {
461             elementToSelect.select();
462             if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
463                 treeOutlineContainerElement.scrollTop = originalScrollTop;
464         }
465
466         this._updateChildrenInProgress = false;
467     }
468
469     adjustCollapsedRange()
470     {
471         // Ensure precondition: only the tree elements for node children are found in the tree
472         // (not the Expand All button or the closing tag).
473         if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
474             this.removeChild(this.expandAllButtonElement.__treeElement);
475
476         if (!this._hasVisibleChildren())
477             return;
478
479         var visibleChildren = this._visibleChildren();
480         var totalChildrenCount = visibleChildren.length;
481
482         // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
483         for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, totalChildrenCount); i < limit; ++i)
484             this.insertChildElement(visibleChildren[i], i);
485
486         var expandedChildCount = this.expandedChildCount;
487         if (totalChildrenCount > this.expandedChildCount) {
488             var targetButtonIndex = expandedChildCount;
489             if (!this.expandAllButtonElement) {
490                 var button = document.createElement("button");
491                 button.className = "show-all-nodes";
492                 button.value = "";
493
494                 var item = new WebInspector.TreeElement(button, null, false);
495                 item.selectable = false;
496                 item.expandAllButton = true;
497
498                 this.insertChild(item, targetButtonIndex);
499                 this.expandAllButtonElement = button;
500                 this.expandAllButtonElement.__treeElement = item;
501                 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
502             } else if (!this.expandAllButtonElement.__treeElement.parent)
503                 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
504
505             this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)").format(totalChildrenCount - expandedChildCount);
506         } else if (this.expandAllButtonElement)
507             this.expandAllButtonElement = null;
508     }
509
510     handleLoadAllChildren()
511     {
512         var visibleChildren = this._visibleChildren();
513         this.expandedChildrenLimit = Math.max(visibleChildren.length, this.expandedChildrenLimit + WebInspector.DOMTreeElement.InitialChildrenLimit);
514     }
515
516     onexpand()
517     {
518         if (this._elementCloseTag)
519             return;
520
521         if (!this.listItemElement)
522             return;
523
524         this.updateTitle();
525     }
526
527     oncollapse()
528     {
529         if (this._elementCloseTag)
530             return;
531
532         this.updateTitle();
533     }
534
535     onreveal()
536     {
537         if (this.listItemElement) {
538             var tagSpans = this.listItemElement.getElementsByClassName("html-tag-name");
539             if (tagSpans.length)
540                 tagSpans[0].scrollIntoViewIfNeeded(false);
541             else
542                 this.listItemElement.scrollIntoViewIfNeeded(false);
543         }
544     }
545
546     onselect(treeElement, selectedByUser)
547     {
548         this.treeOutline.suppressRevealAndSelect = true;
549         this.treeOutline.selectDOMNode(this.representedObject, selectedByUser);
550         if (selectedByUser)
551             WebInspector.domTreeManager.highlightDOMNode(this.representedObject.id);
552         this.treeOutline.updateSelection();
553         this.treeOutline.suppressRevealAndSelect = false;
554     }
555
556     ondeselect(treeElement)
557     {
558         this.treeOutline.selectDOMNode(null);
559     }
560
561     ondelete()
562     {
563         if (!this.editable)
564             return false;
565
566         var startTagTreeElement = this.treeOutline.findTreeElement(this.representedObject);
567         if (startTagTreeElement)
568             startTagTreeElement.remove();
569         else
570             this.remove();
571         return true;
572     }
573
574     onenter()
575     {
576         if (!this.editable)
577             return false;
578
579         // On Enter or Return start editing the first attribute
580         // or create a new attribute on the selected element.
581         if (this.treeOutline.editing)
582             return false;
583
584         this._startEditing();
585
586         // prevent a newline from being immediately inserted
587         return true;
588     }
589
590     selectOnMouseDown(event)
591     {
592         super.selectOnMouseDown(event);
593
594         if (this._editing)
595             return;
596
597         // Prevent selecting the nearest word on double click.
598         if (event.detail >= 2)
599             event.preventDefault();
600     }
601
602     ondblclick(event)
603     {
604         if (!this.editable)
605             return false;
606
607         if (this._editing || this._elementCloseTag)
608             return;
609
610         if (this._startEditingTarget(event.target))
611             return;
612
613         if (this.hasChildren && !this.expanded)
614             this.expand();
615     }
616
617     _insertInLastAttributePosition(tag, node)
618     {
619         if (tag.getElementsByClassName("html-attribute").length > 0)
620             tag.insertBefore(node, tag.lastChild);
621         else {
622             var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
623             tag.textContent = "";
624             tag.append("<" + nodeName, node, ">");
625         }
626
627         this.updateSelectionArea();
628     }
629
630     _startEditingTarget(eventTarget)
631     {
632         if (this.treeOutline.selectedDOMNode() !== this.representedObject)
633             return false;
634
635         if (this.representedObject.isShadowRoot() || this.representedObject.isInUserAgentShadowTree())
636             return false;
637
638         if (this.representedObject.isPseudoElement())
639             return false;
640
641         if (this.representedObject.nodeType() !== Node.ELEMENT_NODE && this.representedObject.nodeType() !== Node.TEXT_NODE)
642             return false;
643
644         var textNode = eventTarget.enclosingNodeOrSelfWithClass("html-text-node");
645         if (textNode)
646             return this._startEditingTextNode(textNode);
647
648         var attribute = eventTarget.enclosingNodeOrSelfWithClass("html-attribute");
649         if (attribute)
650             return this._startEditingAttribute(attribute, eventTarget);
651
652         var tagName = eventTarget.enclosingNodeOrSelfWithClass("html-tag-name");
653         if (tagName)
654             return this._startEditingTagName(tagName);
655
656         return false;
657     }
658
659     _populateTagContextMenu(contextMenu, event)
660     {
661         let node = this.representedObject;
662         if (!node.isInUserAgentShadowTree()) {
663             let attribute = event.target.enclosingNodeOrSelfWithClass("html-attribute");
664
665             if (event.target && event.target.tagName === "A") {
666                 let url = event.target.href;
667
668                 contextMenu.appendItem(WebInspector.UIString("Open in New Tab"), () => {
669                     const frame = null;
670                     WebInspector.openURL(url, frame, {alwaysOpenExternally: true});
671                 });
672
673                 if (WebInspector.frameResourceManager.resourceForURL(url)) {
674                     contextMenu.appendItem(WebInspector.UIString("Reveal in Resources Tab"), () => {
675                         let frame = WebInspector.frameResourceManager.frameForIdentifier(node.frameIdentifier);
676                         WebInspector.openURL(url, frame, {ignoreNetworkTab: true});
677                     });
678                 }
679
680                 contextMenu.appendItem(WebInspector.UIString("Copy Link Address"), () => {
681                     InspectorFrontendHost.copyText(url);
682                 });
683
684                 contextMenu.appendSeparator();
685             }
686
687             // Add attribute-related actions.
688             if (this.editable) {
689                 contextMenu.appendItem(WebInspector.UIString("Add Attribute"), this._addNewAttribute.bind(this));
690                 if (attribute)
691                     contextMenu.appendItem(WebInspector.UIString("Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
692                 contextMenu.appendSeparator();
693             }
694
695             if (WebInspector.cssStyleManager.canForcePseudoClasses()) {
696                 let pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString("Forced Pseudo-Classes"));
697                 this._populateForcedPseudoStateItems(pseudoSubMenu);
698                 contextMenu.appendSeparator();
699             }
700         }
701
702         this._populateNodeContextMenu(contextMenu);
703         this.treeOutline._populateContextMenu(contextMenu, this.representedObject);
704     }
705
706     _populateForcedPseudoStateItems(subMenu)
707     {
708         var node = this.representedObject;
709         var enabledPseudoClasses = node.enabledPseudoClasses;
710         // These strings don't need to be localized as they are CSS pseudo-classes.
711         WebInspector.CSSStyleManager.ForceablePseudoClasses.forEach(function(pseudoClass) {
712             var label = pseudoClass.capitalize();
713             var enabled = enabledPseudoClasses.includes(pseudoClass);
714             subMenu.appendCheckboxItem(label, function() {
715                 node.setPseudoClassEnabled(pseudoClass, !enabled);
716             }, enabled, false);
717         });
718     }
719
720     _populateTextContextMenu(contextMenu, textNode)
721     {
722         if (this.editable)
723             contextMenu.appendItem(WebInspector.UIString("Edit Text"), this._startEditingTextNode.bind(this, textNode));
724
725         this._populateNodeContextMenu(contextMenu);
726     }
727
728     _populateNodeContextMenu(contextMenu)
729     {
730         let node = this.representedObject;
731
732         // Add free-form node-related actions.
733         if (this.editable)
734             contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
735         if (!node.isPseudoElement())
736             contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
737         if (this.editable)
738             contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this));
739         if (node.nodeType() === Node.ELEMENT_NODE)
740             contextMenu.appendItem(WebInspector.UIString("Scroll Into View"), this._scrollIntoView.bind(this));
741
742         contextMenu.appendSeparator();
743
744         if (node.nodeType() === Node.ELEMENT_NODE) {
745             contextMenu.appendItem(WebInspector.UIString("Copy Selector Path"), () => {
746                 let cssPath = WebInspector.cssPath(this.representedObject);
747                 InspectorFrontendHost.copyText(cssPath);
748             });
749         }
750
751         if (!node.isPseudoElement()) {
752             contextMenu.appendItem(WebInspector.UIString("Copy XPath"), () => {
753                 let xpath = WebInspector.xpath(this.representedObject);
754                 InspectorFrontendHost.copyText(xpath);
755             });
756         }
757
758         if (node.isCustomElement()) {
759             contextMenu.appendSeparator();
760             contextMenu.appendItem(WebInspector.UIString("Jump to Definition"), this._showCustomElementDefinition.bind(this));
761         }
762
763         if (WebInspector.domDebuggerManager.supported && node.nodeType() === Node.ELEMENT_NODE) {
764             contextMenu.appendSeparator();
765
766             const allowEditing = false;
767             WebInspector.DOMBreakpointTreeController.appendBreakpointContextMenuItems(contextMenu, node, allowEditing);
768         }
769     }
770
771     _startEditing()
772     {
773         if (this.treeOutline.selectedDOMNode() !== this.representedObject)
774             return false;
775
776         if (!this.editable)
777             return false;
778
779         var listItem = this.listItemElement;
780
781         if (this._canAddAttributes) {
782             var attribute = listItem.getElementsByClassName("html-attribute")[0];
783             if (attribute)
784                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("html-attribute-value")[0]);
785
786             return this._addNewAttribute();
787         }
788
789         if (this.representedObject.nodeType() === Node.TEXT_NODE) {
790             var textNode = listItem.getElementsByClassName("html-text-node")[0];
791             if (textNode)
792                 return this._startEditingTextNode(textNode);
793             return false;
794         }
795     }
796
797     _addNewAttribute()
798     {
799         // Cannot just convert the textual html into an element without
800         // a parent node. Use a temporary span container for the HTML.
801         var container = document.createElement("span");
802         this._buildAttributeDOM(container, " ", "");
803         var attr = container.firstChild;
804         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
805         attr.style.marginRight = "2px"; // overrides the .editing margin rule
806
807         var tag = this.listItemElement.getElementsByClassName("html-tag")[0];
808         this._insertInLastAttributePosition(tag, attr);
809         return this._startEditingAttribute(attr, attr);
810     }
811
812     _triggerEditAttribute(attributeName)
813     {
814         var attributeElements = this.listItemElement.getElementsByClassName("html-attribute-name");
815         for (var i = 0, len = attributeElements.length; i < len; ++i) {
816             if (attributeElements[i].textContent === attributeName) {
817                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
818                     if (elem.nodeType !== Node.ELEMENT_NODE)
819                         continue;
820
821                     if (elem.classList.contains("html-attribute-value"))
822                         return this._startEditingAttribute(elem.parentNode, elem);
823                 }
824             }
825         }
826     }
827
828     _startEditingAttribute(attribute, elementForSelection)
829     {
830         if (WebInspector.isBeingEdited(attribute))
831             return true;
832
833         var attributeNameElement = attribute.getElementsByClassName("html-attribute-name")[0];
834         if (!attributeNameElement)
835             return false;
836
837         var attributeName = attributeNameElement.textContent;
838
839         function removeZeroWidthSpaceRecursive(node)
840         {
841             if (node.nodeType === Node.TEXT_NODE) {
842                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
843                 return;
844             }
845
846             if (node.nodeType !== Node.ELEMENT_NODE)
847                 return;
848
849             for (var child = node.firstChild; child; child = child.nextSibling)
850                 removeZeroWidthSpaceRecursive(child);
851         }
852
853         // Remove zero-width spaces that were added by nodeTitleInfo.
854         removeZeroWidthSpaceRecursive(attribute);
855
856         var config = new WebInspector.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
857         config.setNumberCommitHandler(this._attributeNumberEditingCommitted.bind(this));
858         this._editing = WebInspector.startEditing(attribute, config);
859
860         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
861
862         return true;
863     }
864
865     _startEditingTextNode(textNode)
866     {
867         if (WebInspector.isBeingEdited(textNode))
868             return true;
869
870         var config = new WebInspector.EditingConfig(this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
871         config.spellcheck = true;
872         this._editing = WebInspector.startEditing(textNode, config);
873         window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
874
875         return true;
876     }
877
878     _startEditingTagName(tagNameElement)
879     {
880         if (!tagNameElement) {
881             tagNameElement = this.listItemElement.getElementsByClassName("html-tag-name")[0];
882             if (!tagNameElement)
883                 return false;
884         }
885
886         var tagName = tagNameElement.textContent;
887         if (WebInspector.DOMTreeElement.EditTagBlacklist[tagName.toLowerCase()])
888             return false;
889
890         if (WebInspector.isBeingEdited(tagNameElement))
891             return true;
892
893         let closingTagElement = this._distinctClosingTagElement();
894         let originalClosingTagTextContent = closingTagElement ? closingTagElement.textContent : "";
895
896         function keyupListener(event)
897         {
898             if (closingTagElement)
899                 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
900         }
901
902         function editingComitted(element, newTagName)
903         {
904             tagNameElement.removeEventListener("keyup", keyupListener, false);
905             this._tagNameEditingCommitted.apply(this, arguments);
906         }
907
908         function editingCancelled()
909         {
910             if (closingTagElement)
911                 closingTagElement.textContent = originalClosingTagTextContent;
912
913             tagNameElement.removeEventListener("keyup", keyupListener, false);
914             this._editingCancelled.apply(this, arguments);
915         }
916
917         tagNameElement.addEventListener("keyup", keyupListener, false);
918
919         var config = new WebInspector.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName);
920         this._editing = WebInspector.startEditing(tagNameElement, config);
921         window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
922         return true;
923     }
924
925     _startEditingAsHTML(commitCallback, error, initialValue)
926     {
927         if (error)
928             return;
929         if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement))
930             return;
931
932         this._htmlEditElement = document.createElement("div");
933         this._htmlEditElement.textContent = initialValue;
934
935         // Hide header items.
936         var child = this.listItemElement.firstChild;
937         while (child) {
938             child.style.display = "none";
939             child = child.nextSibling;
940         }
941         // Hide children item.
942         if (this._childrenListNode)
943             this._childrenListNode.style.display = "none";
944         // Append editor.
945         this.listItemElement.appendChild(this._htmlEditElement);
946
947         this.updateSelectionArea();
948
949         function commit()
950         {
951             commitCallback(this._htmlEditElement.textContent);
952             dispose.call(this);
953         }
954
955         function dispose()
956         {
957             this._editing = false;
958
959             // Remove editor.
960             this.listItemElement.removeChild(this._htmlEditElement);
961             this._htmlEditElement = null;
962             // Unhide children item.
963             if (this._childrenListNode)
964                 this._childrenListNode.style.removeProperty("display");
965             // Unhide header items.
966             var child = this.listItemElement.firstChild;
967             while (child) {
968                 child.style.removeProperty("display");
969                 child = child.nextSibling;
970             }
971
972             this.updateSelectionArea();
973         }
974
975         var config = new WebInspector.EditingConfig(commit.bind(this), dispose.bind(this));
976         config.setMultiline(true);
977         this._editing = WebInspector.startEditing(this._htmlEditElement, config);
978     }
979
980     _attributeEditingCommitted(element, newText, oldText, attributeName, moveDirection)
981     {
982         this._editing = false;
983
984         if (!newText.trim())
985             element.remove();
986
987         if (!moveDirection && newText === oldText)
988             return;
989
990         var treeOutline = this.treeOutline;
991         function moveToNextAttributeIfNeeded(error)
992         {
993             if (error)
994                 this._editingCancelled(element, attributeName);
995
996             if (!moveDirection)
997                 return;
998
999             treeOutline._updateModifiedNodes();
1000
1001             // Search for the attribute's position, and then decide where to move to.
1002             var attributes = this.representedObject.attributes();
1003             for (var i = 0; i < attributes.length; ++i) {
1004                 if (attributes[i].name !== attributeName)
1005                     continue;
1006
1007                 if (moveDirection === "backward") {
1008                     if (i === 0)
1009                         this._startEditingTagName();
1010                     else
1011                         this._triggerEditAttribute(attributes[i - 1].name);
1012                 } else {
1013                     if (i === attributes.length - 1)
1014                         this._addNewAttribute();
1015                     else
1016                         this._triggerEditAttribute(attributes[i + 1].name);
1017                 }
1018                 return;
1019             }
1020
1021             // Moving From the "New Attribute" position.
1022             if (moveDirection === "backward") {
1023                 if (newText === " ") {
1024                     // Moving from "New Attribute" that was not edited
1025                     if (attributes.length)
1026                         this._triggerEditAttribute(attributes.lastValue.name);
1027                 } else {
1028                     // Moving from "New Attribute" that holds new value
1029                     if (attributes.length > 1)
1030                         this._triggerEditAttribute(attributes[attributes.length - 2].name);
1031                 }
1032             } else if (moveDirection === "forward") {
1033                 if (!/^\s*$/.test(newText))
1034                     this._addNewAttribute();
1035                 else
1036                     this._startEditingTagName();
1037             }
1038         }
1039
1040         this.representedObject.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
1041     }
1042
1043     _attributeNumberEditingCommitted(element, newText, oldText, attributeName, moveDirection)
1044     {
1045         if (newText === oldText)
1046             return;
1047
1048         this.representedObject.setAttribute(attributeName, newText);
1049     }
1050
1051     _tagNameEditingCommitted(element, newText, oldText, tagName, moveDirection)
1052     {
1053         this._editing = false;
1054         var self = this;
1055
1056         function cancel()
1057         {
1058             var closingTagElement = self._distinctClosingTagElement();
1059             if (closingTagElement)
1060                 closingTagElement.textContent = "</" + tagName + ">";
1061
1062             self._editingCancelled(element, tagName);
1063             moveToNextAttributeIfNeeded.call(self);
1064         }
1065
1066         function moveToNextAttributeIfNeeded()
1067         {
1068             if (moveDirection !== "forward") {
1069                 this._addNewAttribute();
1070                 return;
1071             }
1072
1073             var attributes = this.representedObject.attributes();
1074             if (attributes.length > 0)
1075                 this._triggerEditAttribute(attributes[0].name);
1076             else
1077                 this._addNewAttribute();
1078         }
1079
1080         newText = newText.trim();
1081         if (newText === oldText) {
1082             cancel();
1083             return;
1084         }
1085
1086         var treeOutline = this.treeOutline;
1087         var wasExpanded = this.expanded;
1088
1089         function changeTagNameCallback(error, nodeId)
1090         {
1091             if (error || !nodeId) {
1092                 cancel();
1093                 return;
1094             }
1095
1096             var node = WebInspector.domTreeManager.nodeForId(nodeId);
1097
1098             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1099             treeOutline._updateModifiedNodes();
1100             treeOutline.selectDOMNode(node, true);
1101
1102             var newTreeItem = treeOutline.findTreeElement(node);
1103             if (wasExpanded)
1104                 newTreeItem.expand();
1105
1106             moveToNextAttributeIfNeeded.call(newTreeItem);
1107         }
1108
1109         this.representedObject.setNodeName(newText, changeTagNameCallback);
1110     }
1111
1112     _textNodeEditingCommitted(element, newText)
1113     {
1114         this._editing = false;
1115
1116         var textNode;
1117         if (this.representedObject.nodeType() === Node.ELEMENT_NODE) {
1118             // We only show text nodes inline in elements if the element only
1119             // has a single child, and that child is a text node.
1120             textNode = this.representedObject.firstChild;
1121         } else if (this.representedObject.nodeType() === Node.TEXT_NODE)
1122             textNode = this.representedObject;
1123
1124         textNode.setNodeValue(newText, this.updateTitle.bind(this));
1125     }
1126
1127     _editingCancelled(element, context)
1128     {
1129         this._editing = false;
1130
1131         // Need to restore attributes structure.
1132         this.updateTitle();
1133     }
1134
1135     _distinctClosingTagElement()
1136     {
1137         // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
1138
1139         // For an expanded element, it will be the last element with class "close"
1140         // in the child element list.
1141         if (this.expanded) {
1142             var closers = this._childrenListNode.querySelectorAll(".close");
1143             return closers[closers.length - 1];
1144         }
1145
1146         // Remaining cases are single line non-expanded elements with a closing
1147         // tag, or HTML elements without a closing tag (such as <br>). Return
1148         // null in the case where there isn't a closing tag.
1149         var tags = this.listItemElement.getElementsByClassName("html-tag");
1150         return tags.length === 1 ? null : tags[tags.length - 1];
1151     }
1152
1153     updateTitle(onlySearchQueryChanged)
1154     {
1155         // If we are editing, return early to prevent canceling the edit.
1156         // After editing is committed updateTitle will be called.
1157         if (this._editing)
1158             return;
1159
1160         if (onlySearchQueryChanged) {
1161             if (this._highlightResult)
1162                 this._updateSearchHighlight(false);
1163         } else {
1164             this.title = document.createElement("span");
1165             this.title.appendChild(this._nodeTitleInfo().titleDOM);
1166             this._highlightResult = undefined;
1167         }
1168
1169         // Setting this.title will implicitly remove all children. Clear the
1170         // selection element so that we properly recreate it if necessary.
1171         this._selectionElement = null;
1172         this.updateSelectionArea();
1173         this._highlightSearchResults();
1174         this._updateBreakpointStatus();
1175     }
1176
1177     _buildAttributeDOM(parentElement, name, value, node)
1178     {
1179         let hasText = value.length > 0;
1180         let attrSpanElement = parentElement.createChild("span", "html-attribute");
1181         let attrNameElement = attrSpanElement.createChild("span", "html-attribute-name");
1182         attrNameElement.textContent = name;
1183         let attrValueElement = null;
1184         if (hasText)
1185             attrSpanElement.append("=\u200B\"");
1186
1187         if (name === "src" || /\bhref\b/.test(name)) {
1188             let baseURL = node.ownerDocument ? node.ownerDocument.documentURL : null;
1189             let rewrittenURL = absoluteURL(value, baseURL);
1190             value = value.insertWordBreakCharacters();
1191             if (!rewrittenURL) {
1192                 attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1193                 attrValueElement.textContent = value;
1194             } else {
1195                 if (value.startsWith("data:"))
1196                     value = value.trimMiddle(60);
1197
1198                 attrValueElement = document.createElement("a");
1199                 attrValueElement.href = rewrittenURL;
1200                 attrValueElement.textContent = value;
1201                 attrSpanElement.appendChild(attrValueElement);
1202             }
1203         } else if (name === "srcset") {
1204             let baseURL = node.ownerDocument ? node.ownerDocument.documentURL : null;
1205             attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1206
1207             // Leading whitespace.
1208             let groups = value.split(/\s*,\s*/);
1209             for (let i = 0; i < groups.length; ++i) {
1210                 let string = groups[i].trim();
1211                 let spaceIndex = string.search(/\s/);
1212
1213                 if (spaceIndex === -1) {
1214                     let linkText = string;
1215                     let rewrittenURL = absoluteURL(string, baseURL);
1216                     let linkElement = attrValueElement.appendChild(document.createElement("a"));
1217                     linkElement.href = rewrittenURL;
1218                     linkElement.textContent = linkText.insertWordBreakCharacters();
1219                 } else {
1220                     let linkText = string.substring(0, spaceIndex);
1221                     let descriptorText = string.substring(spaceIndex).insertWordBreakCharacters();
1222                     let rewrittenURL = absoluteURL(linkText, baseURL);
1223                     let linkElement = attrValueElement.appendChild(document.createElement("a"));
1224                     linkElement.href = rewrittenURL;
1225                     linkElement.textContent = linkText.insertWordBreakCharacters();
1226                     let descriptorElement = attrValueElement.appendChild(document.createElement("span"));
1227                     descriptorElement.textContent = descriptorText;
1228                 }
1229
1230                 if (i < groups.length - 1) {
1231                     let commaElement = attrValueElement.appendChild(document.createElement("span"));
1232                     commaElement.textContent = ", ";
1233                 }
1234             }
1235         } else {
1236             value = value.insertWordBreakCharacters();
1237             attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1238             attrValueElement.textContent = value;
1239         }
1240
1241         if (hasText)
1242             attrSpanElement.append("\"");
1243
1244         for (let attribute of this._recentlyModifiedAttributes) {
1245             if (attribute.name === name)
1246                 attribute.element = hasText ? attrValueElement : attrNameElement;
1247         }
1248     }
1249
1250     _buildTagDOM(parentElement, tagName, isClosingTag, isDistinctTreeElement)
1251     {
1252         var node = this.representedObject;
1253         var classes = ["html-tag"];
1254         if (isClosingTag && isDistinctTreeElement)
1255             classes.push("close");
1256         var tagElement = parentElement.createChild("span", classes.join(" "));
1257         tagElement.append("<");
1258         var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "html-tag-name");
1259         tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
1260         if (!isClosingTag && node.hasAttributes()) {
1261             var attributes = node.attributes();
1262             for (var i = 0; i < attributes.length; ++i) {
1263                 var attr = attributes[i];
1264                 tagElement.append(" ");
1265                 this._buildAttributeDOM(tagElement, attr.name, attr.value, node);
1266             }
1267         }
1268         tagElement.append(">");
1269         parentElement.append("\u200B");
1270     }
1271
1272     _nodeTitleInfo()
1273     {
1274         var node = this.representedObject;
1275         var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
1276
1277         function trimedNodeValue()
1278         {
1279             // Trim empty lines from the beginning and extra space at the end since most style and script tags begin with a newline
1280             // and end with a newline and indentation for the end tag.
1281             return node.nodeValue().replace(/^[\n\r]*/, "").replace(/\s*$/, "");
1282         }
1283
1284         switch (node.nodeType()) {
1285             case Node.DOCUMENT_FRAGMENT_NODE:
1286                 var fragmentElement = info.titleDOM.createChild("span", "html-fragment");
1287                 if (node.shadowRootType()) {
1288                     fragmentElement.textContent = WebInspector.UIString("Shadow Content (%s)").format(WebInspector.DOMTreeElement.shadowRootTypeDisplayName(node.shadowRootType()));
1289                     this.listItemElement.classList.add("shadow");
1290                 } else if (node.parentNode && node.parentNode.templateContent() === node) {
1291                     fragmentElement.textContent = WebInspector.UIString("Template Content");
1292                     this.listItemElement.classList.add("template");
1293                 } else {
1294                     fragmentElement.textContent = WebInspector.UIString("Document Fragment");
1295                     this.listItemElement.classList.add("fragment");
1296                 }
1297                 break;
1298
1299             case Node.ATTRIBUTE_NODE:
1300                 var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
1301                 this._buildAttributeDOM(info.titleDOM, node.name, value);
1302                 break;
1303
1304             case Node.ELEMENT_NODE:
1305                 if (node.isPseudoElement()) {
1306                     var pseudoElement = info.titleDOM.createChild("span", "html-pseudo-element");
1307                     pseudoElement.textContent = "::" + node.pseudoType();
1308                     info.titleDOM.appendChild(document.createTextNode("\u200B"));
1309                     info.hasChildren = false;
1310                     break;
1311                 }
1312
1313                 var tagName = node.nodeNameInCorrectCase();
1314                 if (this._elementCloseTag) {
1315                     this._buildTagDOM(info.titleDOM, tagName, true, true);
1316                     info.hasChildren = false;
1317                     break;
1318                 }
1319
1320                 this._buildTagDOM(info.titleDOM, tagName, false, false);
1321
1322                 var textChild = this._singleTextChild(node);
1323                 var showInlineText = textChild && textChild.nodeValue().length < WebInspector.DOMTreeElement.MaximumInlineTextChildLength;
1324
1325                 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.DOMTreeElement.ForbiddenClosingTagElements[tagName]))) {
1326                     if (this.hasChildren) {
1327                         var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1328                         textNodeElement.textContent = ellipsis;
1329                         info.titleDOM.append("\u200B");
1330                     }
1331                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1332                 }
1333
1334                 // If this element only has a single child that is a text node,
1335                 // just show that text and the closing tag inline rather than
1336                 // create a subtree for them
1337                 if (showInlineText) {
1338                     var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1339                     var nodeNameLowerCase = node.nodeName().toLowerCase();
1340
1341                     if (nodeNameLowerCase === "script")
1342                         textNodeElement.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/javascript"));
1343                     else if (nodeNameLowerCase === "style")
1344                         textNodeElement.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/css"));
1345                     else
1346                         textNodeElement.textContent = textChild.nodeValue();
1347
1348                     info.titleDOM.append("\u200B");
1349
1350                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1351                     info.hasChildren = false;
1352                 }
1353                 break;
1354
1355             case Node.TEXT_NODE:
1356                 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
1357                     var newNode = info.titleDOM.createChild("span", "html-text-node large");
1358                     newNode.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/javascript"));
1359                 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
1360                     var newNode = info.titleDOM.createChild("span", "html-text-node large");
1361                     newNode.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/css"));
1362                 } else {
1363                     info.titleDOM.append("\"");
1364                     var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1365                     textNodeElement.textContent = node.nodeValue();
1366                     info.titleDOM.append("\"");
1367                 }
1368                 break;
1369
1370             case Node.COMMENT_NODE:
1371                 var commentElement = info.titleDOM.createChild("span", "html-comment");
1372                 commentElement.append("<!--" + node.nodeValue() + "-->");
1373                 break;
1374
1375             case Node.DOCUMENT_TYPE_NODE:
1376                 var docTypeElement = info.titleDOM.createChild("span", "html-doctype");
1377                 docTypeElement.append("<!DOCTYPE " + node.nodeName());
1378                 if (node.publicId) {
1379                     docTypeElement.append(" PUBLIC \"" + node.publicId + "\"");
1380                     if (node.systemId)
1381                         docTypeElement.append(" \"" + node.systemId + "\"");
1382                 } else if (node.systemId)
1383                     docTypeElement.append(" SYSTEM \"" + node.systemId + "\"");
1384
1385                 docTypeElement.append(">");
1386                 break;
1387
1388             case Node.CDATA_SECTION_NODE:
1389                 var cdataElement = info.titleDOM.createChild("span", "html-text-node");
1390                 cdataElement.append("<![CDATA[" + node.nodeValue() + "]]>");
1391                 break;
1392
1393             case Node.PROCESSING_INSTRUCTION_NODE:
1394                 var processingInstructionElement = info.titleDOM.createChild("span", "html-processing-instruction");
1395                 var data = node.nodeValue();
1396                 var dataString = data.length ? " " + data : "";
1397                 var title = "<?" + node.nodeNameInCorrectCase() + dataString + "?>";
1398                 processingInstructionElement.append(title);
1399                 break;
1400
1401             default:
1402                 info.titleDOM.append(node.nodeNameInCorrectCase().collapseWhitespace());
1403         }
1404
1405         return info;
1406     }
1407
1408     _singleTextChild(node)
1409     {
1410         if (!node)
1411             return null;
1412
1413         var firstChild = node.firstChild;
1414         if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE)
1415             return null;
1416
1417         if (node.hasShadowRoots())
1418             return null;
1419         if (node.templateContent())
1420             return null;
1421         if (node.hasPseudoElements())
1422             return null;
1423
1424         var sibling = firstChild.nextSibling;
1425         return sibling ? null : firstChild;
1426     }
1427
1428     _showInlineText(node)
1429     {
1430         if (node.nodeType() === Node.ELEMENT_NODE) {
1431             var textChild = this._singleTextChild(node);
1432             if (textChild && textChild.nodeValue().length < WebInspector.DOMTreeElement.MaximumInlineTextChildLength)
1433                 return true;
1434         }
1435         return false;
1436     }
1437
1438     _hasVisibleChildren()
1439     {
1440         var node = this.representedObject;
1441
1442         if (this._showInlineText(node))
1443             return false;
1444
1445         if (node.hasChildNodes())
1446             return true;
1447         if (node.templateContent())
1448             return true;
1449         if (node.hasPseudoElements())
1450             return true;
1451
1452         return false;
1453     }
1454
1455     _visibleChildren()
1456     {
1457         var node = this.representedObject;
1458
1459         var visibleChildren = [];
1460
1461         var templateContent = node.templateContent();
1462         if (templateContent)
1463             visibleChildren.push(templateContent);
1464
1465         var beforePseudoElement = node.beforePseudoElement();
1466         if (beforePseudoElement)
1467             visibleChildren.push(beforePseudoElement);
1468
1469         if (node.childNodeCount && node.children)
1470             visibleChildren = visibleChildren.concat(node.children);
1471
1472         var afterPseudoElement = node.afterPseudoElement();
1473         if (afterPseudoElement)
1474             visibleChildren.push(afterPseudoElement);
1475
1476         return visibleChildren;
1477     }
1478
1479     remove()
1480     {
1481         var parentElement = this.parent;
1482         if (!parentElement)
1483             return;
1484
1485         var self = this;
1486         function removeNodeCallback(error, removedNodeId)
1487         {
1488             if (error)
1489                 return;
1490
1491             if (!self.parent)
1492                 return;
1493
1494             parentElement.removeChild(self);
1495             parentElement.adjustCollapsedRange();
1496         }
1497
1498         this.representedObject.removeNode(removeNodeCallback);
1499     }
1500
1501     _scrollIntoView()
1502     {
1503         function resolvedNode(object)
1504         {
1505             if (!object)
1506                 return;
1507
1508             function scrollIntoView()
1509             {
1510                 this.scrollIntoViewIfNeeded(true);
1511             }
1512
1513             object.callFunction(scrollIntoView, undefined, false, function() {});
1514             object.release();
1515         }
1516
1517         let node = this.representedObject;
1518         WebInspector.RemoteObject.resolveNode(node, "", resolvedNode);
1519     }
1520
1521     _showCustomElementDefinition()
1522     {
1523         const node = this.representedObject;
1524         WebInspector.RemoteObject.resolveNode(node, "", (remoteObject) => {
1525             if (!remoteObject)
1526                 return;
1527
1528             remoteObject.getProperty("constructor", (error, result, wasThrown) => {
1529                 if (error || result.type !== "function")
1530                     return;
1531
1532                 DebuggerAgent.getFunctionDetails(result.objectId, (error, response) => {
1533                     if (error)
1534                         return;
1535
1536                     let location = response.location;
1537                     let sourceCode = WebInspector.debuggerManager.scriptForIdentifier(location.scriptId, WebInspector.mainTarget);
1538                     if (!sourceCode)
1539                         return;
1540
1541                     let sourceCodeLocation = sourceCode.createSourceCodeLocation(location.lineNumber, location.columnNumber || 0);
1542                     WebInspector.showSourceCodeLocation(sourceCodeLocation, {ignoreNetworkTab: true});
1543                 });
1544                 result.release();
1545             });
1546             remoteObject.release();
1547         });
1548     }
1549
1550     _editAsHTML()
1551     {
1552         var treeOutline = this.treeOutline;
1553         var node = this.representedObject;
1554         var parentNode = node.parentNode;
1555         var index = node.index;
1556         var wasExpanded = this.expanded;
1557
1558         function selectNode(error, nodeId)
1559         {
1560             if (error)
1561                 return;
1562
1563             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1564             treeOutline._updateModifiedNodes();
1565
1566             var newNode = parentNode ? parentNode.children[index] || parentNode : null;
1567             if (!newNode)
1568                 return;
1569
1570             treeOutline.selectDOMNode(newNode, true);
1571
1572             if (wasExpanded) {
1573                 var newTreeItem = treeOutline.findTreeElement(newNode);
1574                 if (newTreeItem)
1575                     newTreeItem.expand();
1576             }
1577         }
1578
1579         function commitChange(value)
1580         {
1581             node.setOuterHTML(value, selectNode);
1582         }
1583
1584         node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
1585     }
1586
1587     _copyHTML()
1588     {
1589         this.representedObject.copyNode();
1590     }
1591
1592     _highlightSearchResults()
1593     {
1594         if (!this.title || !this._searchQuery || !this._searchHighlightsVisible)
1595             return;
1596
1597         if (this._highlightResult) {
1598             this._updateSearchHighlight(true);
1599             return;
1600         }
1601
1602         var text = this.title.textContent;
1603         var searchRegex = new RegExp(this._searchQuery.escapeForRegExp(), "gi");
1604
1605         var match = searchRegex.exec(text);
1606         var matchRanges = [];
1607         while (match) {
1608             matchRanges.push({offset: match.index, length: match[0].length});
1609             match = searchRegex.exec(text);
1610         }
1611
1612         // Fall back for XPath, etc. matches.
1613         if (!matchRanges.length)
1614             matchRanges.push({offset: 0, length: text.length});
1615
1616         this._highlightResult = [];
1617         WebInspector.highlightRangesWithStyleClass(this.title, matchRanges, WebInspector.DOMTreeElement.SearchHighlightStyleClassName, this._highlightResult);
1618     }
1619
1620     _markNodeChanged()
1621     {
1622         for (let attribute of this._recentlyModifiedAttributes) {
1623             let element = attribute.element;
1624             if (!element)
1625                 continue;
1626
1627             element.classList.remove("node-state-changed");
1628             element.addEventListener("animationend", this._boundNodeChangedAnimationEnd);
1629             element.classList.add("node-state-changed");
1630         }
1631     }
1632
1633     _nodeChangedAnimationEnd(event)
1634     {
1635         let element = event.target;
1636         element.classList.remove("node-state-changed");
1637         element.removeEventListener("animationend", this._boundNodeChangedAnimationEnd);
1638
1639         for (let i = this._recentlyModifiedAttributes.length - 1; i >= 0; --i) {
1640             if (this._recentlyModifiedAttributes[i].element === element)
1641                 this._recentlyModifiedAttributes.splice(i, 1);
1642         }
1643     }
1644
1645     get pseudoClassesEnabled()
1646     {
1647         return !!this.representedObject.enabledPseudoClasses.length;
1648     }
1649
1650     _nodePseudoClassesDidChange(event)
1651     {
1652         if (this._elementCloseTag)
1653             return;
1654
1655         this.updateSelectionArea();
1656         this.listItemElement.classList.toggle("pseudo-class-enabled", !!this.representedObject.enabledPseudoClasses.length);
1657     }
1658
1659     _fireDidChange()
1660     {
1661         super._fireDidChange();
1662
1663         this._markNodeChanged();
1664     }
1665
1666     handleEvent(event)
1667     {
1668         if (event.type === "dragstart" && this._editing)
1669             event.preventDefault();
1670     }
1671
1672     _updateBreakpointStatus()
1673     {
1674         let listItemElement = this.listItemElement;
1675         if (!listItemElement)
1676             return;
1677
1678         if (this._breakpointStatus === WebInspector.DOMTreeElement.BreakpointStatus.None) {
1679             if (this._statusImageElement)
1680                 this._statusImageElement.remove();
1681             return;
1682         }
1683
1684         if (!this._statusImageElement) {
1685             this._statusImageElement = useSVGSymbol("Images/DOMBreakpoint.svg", "status-image");
1686             this._statusImageElement.classList.add("breakpoint");
1687             this._statusImageElement.addEventListener("click", this._statusImageClicked.bind(this));
1688             this._statusImageElement.addEventListener("contextmenu", this._statusImageContextmenu.bind(this));
1689             this._statusImageElement.addEventListener("mousedown", (event) => { event.stopPropagation(); });
1690         }
1691
1692         this.listItemElement.insertBefore(this._statusImageElement, this.listItemElement.firstChild);
1693
1694         let disabled = this._breakpointStatus === WebInspector.DOMTreeElement.BreakpointStatus.DisabledBreakpoint;
1695         this._statusImageElement.classList.toggle("disabled", disabled);
1696     }
1697
1698     _statusImageClicked(event)
1699     {
1700         if (event.button !== 0 || event.ctrlKey)
1701             return;
1702
1703         let breakpoints = WebInspector.domDebuggerManager.domBreakpointsForNode(this.representedObject);
1704         if (!breakpoints || !breakpoints.length)
1705             return;
1706
1707         let shouldEnable = breakpoints.some((breakpoint) => breakpoint.disabled);
1708         breakpoints.forEach((breakpoint) => breakpoint.disabled = !shouldEnable);
1709     }
1710
1711     _statusImageContextmenu(event)
1712     {
1713         const allowEditing = true;
1714
1715         let contextMenu = WebInspector.ContextMenu.createFromEvent(event);
1716         WebInspector.DOMBreakpointTreeController.appendBreakpointContextMenuItems(contextMenu, this.representedObject, allowEditing);
1717     }
1718 };
1719
1720 WebInspector.DOMTreeElement.InitialChildrenLimit = 500;
1721 WebInspector.DOMTreeElement.MaximumInlineTextChildLength = 80;
1722
1723 // A union of HTML4 and HTML5-Draft elements that explicitly
1724 // or implicitly (for HTML5) forbid the closing tag.
1725 WebInspector.DOMTreeElement.ForbiddenClosingTagElements = [
1726     "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
1727     "hr", "img", "input", "keygen", "link", "meta", "param", "source",
1728     "wbr", "track", "menuitem"
1729 ].keySet();
1730
1731 // These tags we do not allow editing their tag name.
1732 WebInspector.DOMTreeElement.EditTagBlacklist = [
1733     "html", "head", "body"
1734 ].keySet();
1735
1736 WebInspector.DOMTreeElement.ChangeType = {
1737     Attribute: "dom-tree-element-change-type-attribute"
1738 };
1739
1740 WebInspector.DOMTreeElement.BreakpointStatus = {
1741     None: Symbol("none"),
1742     Breakpoint: Symbol("breakpoint"),
1743     DisabledBreakpoint: Symbol("disabled-breakpoint"),
1744 };
1745
1746 WebInspector.DOMTreeElement.SearchHighlightStyleClassName = "search-highlight";
1747 WebInspector.DOMTreeElement.BouncyHighlightStyleClassName = "bouncy-highlight";