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