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