releasePointerCapture() not working for implicit capture; can't opt-in to pointerente...
[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) {
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.treeOutline.editable) {
859             if (this.selected && this.treeOutline && this.treeOutline.selectedTreeElements.length > 1) {
860                 let forceHidden = !this.treeOutline.selectedTreeElements.every((treeElement) => treeElement.isNodeHidden);
861                 let label = forceHidden ? WI.UIString("Hide Elements") : WI.UIString("Show Elements");
862                 contextMenu.appendItem(label, () => {
863                     this.treeOutline.toggleSelectedElementsVisibility(forceHidden);
864                 });
865             } else if (isEditableNode) {
866                 contextMenu.appendItem(WI.UIString("Toggle Visibility"), () => {
867                     this.toggleElementVisibility();
868                 });
869             }
870         }
871     }
872
873     _startEditing()
874     {
875         if (this.treeOutline.selectedDOMNode() !== this.representedObject)
876             return false;
877
878         if (!this.editable)
879             return false;
880
881         var listItem = this.listItemElement;
882
883         if (this._canAddAttributes) {
884             var attribute = listItem.getElementsByClassName("html-attribute")[0];
885             if (attribute)
886                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("html-attribute-value")[0]);
887
888             return this._addNewAttribute();
889         }
890
891         if (this.representedObject.nodeType() === Node.TEXT_NODE) {
892             var textNode = listItem.getElementsByClassName("html-text-node")[0];
893             if (textNode)
894                 return this._startEditingTextNode(textNode);
895             return false;
896         }
897     }
898
899     _addNewAttribute()
900     {
901         // Cannot just convert the textual html into an element without
902         // a parent node. Use a temporary span container for the HTML.
903         var container = document.createElement("span");
904         this._buildAttributeDOM(container, " ", "");
905         var attr = container.firstChild;
906         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
907         attr.style.marginRight = "2px"; // overrides the .editing margin rule
908
909         var tag = this.listItemElement.getElementsByClassName("html-tag")[0];
910         this._insertInLastAttributePosition(tag, attr);
911         return this._startEditingAttribute(attr, attr);
912     }
913
914     _triggerEditAttribute(attributeName)
915     {
916         var attributeElements = this.listItemElement.getElementsByClassName("html-attribute-name");
917         for (var i = 0, len = attributeElements.length; i < len; ++i) {
918             if (attributeElements[i].textContent === attributeName) {
919                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
920                     if (elem.nodeType !== Node.ELEMENT_NODE)
921                         continue;
922
923                     if (elem.classList.contains("html-attribute-value"))
924                         return this._startEditingAttribute(elem.parentNode, elem);
925                 }
926             }
927         }
928     }
929
930     _startEditingAttribute(attribute, elementForSelection)
931     {
932         if (WI.isBeingEdited(attribute))
933             return true;
934
935         var attributeNameElement = attribute.getElementsByClassName("html-attribute-name")[0];
936         if (!attributeNameElement)
937             return false;
938
939         var attributeName = attributeNameElement.textContent;
940
941         function removeZeroWidthSpaceRecursive(node)
942         {
943             if (node.nodeType === Node.TEXT_NODE) {
944                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
945                 return;
946             }
947
948             if (node.nodeType !== Node.ELEMENT_NODE)
949                 return;
950
951             for (var child = node.firstChild; child; child = child.nextSibling)
952                 removeZeroWidthSpaceRecursive(child);
953         }
954
955         // Remove zero-width spaces that were added by nodeTitleInfo.
956         removeZeroWidthSpaceRecursive(attribute);
957
958         var config = new WI.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
959         config.setNumberCommitHandler(this._attributeNumberEditingCommitted.bind(this));
960         this._editing = WI.startEditing(attribute, config);
961
962         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
963
964         return true;
965     }
966
967     _startEditingTextNode(textNode)
968     {
969         if (WI.isBeingEdited(textNode))
970             return true;
971
972         var config = new WI.EditingConfig(this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
973         config.spellcheck = true;
974         this._editing = WI.startEditing(textNode, config);
975         window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
976
977         return true;
978     }
979
980     _startEditingTagName(tagNameElement)
981     {
982         if (!tagNameElement) {
983             tagNameElement = this.listItemElement.getElementsByClassName("html-tag-name")[0];
984             if (!tagNameElement)
985                 return false;
986         }
987
988         var tagName = tagNameElement.textContent;
989         if (WI.DOMTreeElement.EditTagBlacklist.has(tagName.toLowerCase()))
990             return false;
991
992         if (WI.isBeingEdited(tagNameElement))
993             return true;
994
995         let closingTagElement = this._distinctClosingTagElement();
996         let originalClosingTagTextContent = closingTagElement ? closingTagElement.textContent : "";
997
998         function keyupListener(event)
999         {
1000             if (closingTagElement)
1001                 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
1002         }
1003
1004         function editingComitted(element, newTagName)
1005         {
1006             tagNameElement.removeEventListener("keyup", keyupListener, false);
1007             this._tagNameEditingCommitted.apply(this, arguments);
1008         }
1009
1010         function editingCancelled()
1011         {
1012             if (closingTagElement)
1013                 closingTagElement.textContent = originalClosingTagTextContent;
1014
1015             tagNameElement.removeEventListener("keyup", keyupListener, false);
1016             this._editingCancelled.apply(this, arguments);
1017         }
1018
1019         tagNameElement.addEventListener("keyup", keyupListener, false);
1020
1021         var config = new WI.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName);
1022         this._editing = WI.startEditing(tagNameElement, config);
1023         window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
1024         return true;
1025     }
1026
1027     _startEditingAsHTML(commitCallback, options = {})
1028     {
1029         if (this._htmlEditElement && WI.isBeingEdited(this._htmlEditElement))
1030             return;
1031
1032         if (options.hideExistingElements) {
1033             let child = this.listItemElement.firstChild;
1034             while (child) {
1035                 child.style.display = "none";
1036                 child = child.nextSibling;
1037             }
1038             if (this._childrenListNode)
1039                 this._childrenListNode.style.display = "none";
1040         }
1041
1042         let positionInside = options.position === "afterbegin" || options.position === "beforeend";
1043         if (positionInside && this._childrenListNode) {
1044             this._htmlEditElement = document.createElement("li");
1045
1046             let referenceNode = options.position === "afterbegin" ? this._childrenListNode.firstElementChild : this._childrenListNode.lastElementChild;
1047             this._childrenListNode.insertBefore(this._htmlEditElement, referenceNode);
1048         } else if (options.position && !positionInside) {
1049             this._htmlEditElement = document.createElement("li");
1050
1051             let targetNode = (options.position === "afterend" && this._childrenListNode) ? this._childrenListNode : this.listItemElement;
1052             targetNode.insertAdjacentElement(options.position, this._htmlEditElement);
1053         } else {
1054             this._htmlEditElement = document.createElement("div");
1055             this.listItemElement.appendChild(this._htmlEditElement);
1056         }
1057
1058         if (options.initialValue)
1059             this._htmlEditElement.textContent = options.initialValue;
1060
1061         this.updateSelectionArea();
1062
1063         function commit()
1064         {
1065             commitCallback(this._htmlEditElement.textContent);
1066             dispose.call(this);
1067         }
1068
1069         function dispose()
1070         {
1071             this._editing = false;
1072
1073             // Remove editor.
1074             this._htmlEditElement.remove();
1075             this._htmlEditElement = null;
1076
1077             if (options.hideExistingElements) {
1078                 if (this._childrenListNode)
1079                     this._childrenListNode.style.removeProperty("display");
1080                 let child = this.listItemElement.firstChild;
1081                 while (child) {
1082                     child.style.removeProperty("display");
1083                     child = child.nextSibling;
1084                 }
1085             }
1086
1087             this.updateSelectionArea();
1088         }
1089
1090         var config = new WI.EditingConfig(commit.bind(this), dispose.bind(this));
1091         config.setMultiline(true);
1092         this._editing = WI.startEditing(this._htmlEditElement, config);
1093
1094         if (options.initialValue && !isNaN(options.startPosition)) {
1095             let range = document.createRange();
1096             range.setStart(this._htmlEditElement.firstChild, options.startPosition);
1097             range.collapse(true);
1098
1099             let selection = window.getSelection();
1100             selection.removeAllRanges();
1101             selection.addRange(range);
1102         }
1103     }
1104
1105     _attributeEditingCommitted(element, newText, oldText, attributeName, moveDirection)
1106     {
1107         this._editing = false;
1108
1109         if (!newText.trim())
1110             element.remove();
1111
1112         if (!moveDirection && newText === oldText)
1113             return;
1114
1115         // FIXME: Workaround for <https://webkit.org/b/123163> &nbsp; is forced on SPACE between text nodes.
1116         const nbspRegex = /\xA0/g;
1117         newText = newText.replace(nbspRegex, " ");
1118
1119         var treeOutline = this.treeOutline;
1120         function moveToNextAttributeIfNeeded(error)
1121         {
1122             if (error)
1123                 this._editingCancelled(element, attributeName);
1124
1125             if (!moveDirection)
1126                 return;
1127
1128             treeOutline._updateModifiedNodes();
1129
1130             // Search for the attribute's position, and then decide where to move to.
1131             var attributes = this.representedObject.attributes();
1132             for (var i = 0; i < attributes.length; ++i) {
1133                 if (attributes[i].name !== attributeName)
1134                     continue;
1135
1136                 if (moveDirection === "backward") {
1137                     if (i === 0)
1138                         this._startEditingTagName();
1139                     else
1140                         this._triggerEditAttribute(attributes[i - 1].name);
1141                 } else {
1142                     if (i === attributes.length - 1)
1143                         this._addNewAttribute();
1144                     else
1145                         this._triggerEditAttribute(attributes[i + 1].name);
1146                 }
1147                 return;
1148             }
1149
1150             // Moving From the "New Attribute" position.
1151             if (moveDirection === "backward") {
1152                 if (newText === " ") {
1153                     // Moving from "New Attribute" that was not edited
1154                     if (attributes.length)
1155                         this._triggerEditAttribute(attributes.lastValue.name);
1156                 } else {
1157                     // Moving from "New Attribute" that holds new value
1158                     if (attributes.length > 1)
1159                         this._triggerEditAttribute(attributes[attributes.length - 2].name);
1160                 }
1161             } else if (moveDirection === "forward") {
1162                 if (!/^\s*$/.test(newText))
1163                     this._addNewAttribute();
1164                 else
1165                     this._startEditingTagName();
1166             }
1167         }
1168
1169         this.representedObject.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
1170     }
1171
1172     _attributeNumberEditingCommitted(element, newText, oldText, attributeName, moveDirection)
1173     {
1174         if (newText === oldText)
1175             return;
1176
1177         this.representedObject.setAttribute(attributeName, newText);
1178     }
1179
1180     _tagNameEditingCommitted(element, newText, oldText, tagName, moveDirection)
1181     {
1182         this._editing = false;
1183         var self = this;
1184
1185         function cancel()
1186         {
1187             var closingTagElement = self._distinctClosingTagElement();
1188             if (closingTagElement)
1189                 closingTagElement.textContent = "</" + tagName + ">";
1190
1191             self._editingCancelled(element, tagName);
1192             moveToNextAttributeIfNeeded.call(self);
1193         }
1194
1195         function moveToNextAttributeIfNeeded()
1196         {
1197             if (moveDirection !== "forward") {
1198                 this._addNewAttribute();
1199                 return;
1200             }
1201
1202             var attributes = this.representedObject.attributes();
1203             if (attributes.length > 0)
1204                 this._triggerEditAttribute(attributes[0].name);
1205             else
1206                 this._addNewAttribute();
1207         }
1208
1209         newText = newText.trim();
1210         if (newText === oldText) {
1211             cancel();
1212             return;
1213         }
1214
1215         var treeOutline = this.treeOutline;
1216         var wasExpanded = this.expanded;
1217
1218         function changeTagNameCallback(error, nodeId)
1219         {
1220             if (error || !nodeId) {
1221                 cancel();
1222                 return;
1223             }
1224
1225             var node = WI.domManager.nodeForId(nodeId);
1226
1227             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1228             treeOutline._updateModifiedNodes();
1229             treeOutline.selectDOMNode(node, true);
1230
1231             var newTreeItem = treeOutline.findTreeElement(node);
1232             if (wasExpanded)
1233                 newTreeItem.expand();
1234
1235             moveToNextAttributeIfNeeded.call(newTreeItem);
1236         }
1237
1238         this.representedObject.setNodeName(newText, changeTagNameCallback);
1239     }
1240
1241     _textNodeEditingCommitted(element, newText)
1242     {
1243         this._editing = false;
1244
1245         var textNode;
1246         if (this.representedObject.nodeType() === Node.ELEMENT_NODE) {
1247             // We only show text nodes inline in elements if the element only
1248             // has a single child, and that child is a text node.
1249             textNode = this.representedObject.firstChild;
1250         } else if (this.representedObject.nodeType() === Node.TEXT_NODE)
1251             textNode = this.representedObject;
1252
1253         textNode.setNodeValue(newText, this.updateTitle.bind(this));
1254     }
1255
1256     _editingCancelled(element, context)
1257     {
1258         this._editing = false;
1259
1260         // Need to restore attributes structure.
1261         this.updateTitle();
1262     }
1263
1264     _distinctClosingTagElement()
1265     {
1266         // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
1267
1268         // For an expanded element, it will be the last element with class "close"
1269         // in the child element list.
1270         if (this.expanded) {
1271             var closers = this._childrenListNode.querySelectorAll(".close");
1272             return closers[closers.length - 1];
1273         }
1274
1275         // Remaining cases are single line non-expanded elements with a closing
1276         // tag, or HTML elements without a closing tag (such as <br>). Return
1277         // null in the case where there isn't a closing tag.
1278         var tags = this.listItemElement.getElementsByClassName("html-tag");
1279         return tags.length === 1 ? null : tags[tags.length - 1];
1280     }
1281
1282     updateTitle(onlySearchQueryChanged)
1283     {
1284         // If we are editing, return early to prevent canceling the edit.
1285         // After editing is committed updateTitle will be called.
1286         if (this._editing && !this._forceUpdateTitle)
1287             return;
1288
1289         if (onlySearchQueryChanged) {
1290             if (this._highlightResult)
1291                 this._updateSearchHighlight(false);
1292         } else {
1293             this.title = document.createElement("span");
1294             this.title.appendChild(this._nodeTitleInfo().titleDOM);
1295             this._highlightResult = undefined;
1296         }
1297
1298         // Setting this.title will implicitly remove all children. Clear the
1299         // selection element so that we properly recreate it if necessary.
1300         this._selectionElement = null;
1301         this.updateSelectionArea();
1302         this._highlightSearchResults();
1303         this._updatePseudoClassIndicator();
1304         this._updateBreakpointStatus();
1305     }
1306
1307     _buildAttributeDOM(parentElement, name, value, node)
1308     {
1309         let hasText = value.length > 0;
1310         let attrSpanElement = parentElement.createChild("span", "html-attribute");
1311         let attrNameElement = attrSpanElement.createChild("span", "html-attribute-name");
1312         attrNameElement.textContent = name;
1313         let attrValueElement = null;
1314         if (hasText)
1315             attrSpanElement.append("=\u200B\"");
1316
1317         if (name === "src" || /\bhref\b/.test(name)) {
1318             let baseURL = node.frame ? node.frame.url : null;
1319             let rewrittenURL = absoluteURL(value, baseURL);
1320             value = value.insertWordBreakCharacters();
1321             if (!rewrittenURL) {
1322                 attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1323                 attrValueElement.textContent = value;
1324             } else {
1325                 if (value.startsWith("data:"))
1326                     value = value.truncateMiddle(60);
1327
1328                 attrValueElement = document.createElement("a");
1329                 attrValueElement.href = rewrittenURL;
1330                 attrValueElement.textContent = value;
1331                 attrSpanElement.appendChild(attrValueElement);
1332             }
1333         } else if (name === "srcset") {
1334             let baseURL = node.frame ? node.frame.url : null;
1335             attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1336
1337             // Leading whitespace.
1338             let groups = value.split(/\s*,\s*/);
1339             for (let i = 0; i < groups.length; ++i) {
1340                 let string = groups[i].trim();
1341                 let spaceIndex = string.search(/\s/);
1342
1343                 if (spaceIndex === -1) {
1344                     let linkText = string;
1345                     let rewrittenURL = absoluteURL(string, baseURL);
1346                     let linkElement = attrValueElement.appendChild(document.createElement("a"));
1347                     linkElement.href = rewrittenURL;
1348                     linkElement.textContent = linkText.insertWordBreakCharacters();
1349                 } else {
1350                     let linkText = string.substring(0, spaceIndex);
1351                     let descriptorText = string.substring(spaceIndex).insertWordBreakCharacters();
1352                     let rewrittenURL = absoluteURL(linkText, baseURL);
1353                     let linkElement = attrValueElement.appendChild(document.createElement("a"));
1354                     linkElement.href = rewrittenURL;
1355                     linkElement.textContent = linkText.insertWordBreakCharacters();
1356                     let descriptorElement = attrValueElement.appendChild(document.createElement("span"));
1357                     descriptorElement.textContent = descriptorText;
1358                 }
1359
1360                 if (i < groups.length - 1) {
1361                     let commaElement = attrValueElement.appendChild(document.createElement("span"));
1362                     commaElement.textContent = ", ";
1363                 }
1364             }
1365         } else {
1366             value = value.insertWordBreakCharacters();
1367             attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1368             attrValueElement.textContent = value;
1369         }
1370
1371         if (hasText)
1372             attrSpanElement.append("\"");
1373
1374         this._createModifiedAnimation(name, value, hasText ? attrValueElement : attrNameElement);
1375
1376         if (this._highlightedAttributes.has(name))
1377             attrSpanElement.classList.add("highlight");
1378     }
1379
1380     _buildTagDOM({parentElement, tagName, isClosingTag, isDistinctTreeElement, willRenderCloseTagInline})
1381     {
1382         var node = this.representedObject;
1383         var classes = ["html-tag"];
1384         if (isClosingTag && isDistinctTreeElement)
1385             classes.push("close");
1386         var tagElement = parentElement.createChild("span", classes.join(" "));
1387         tagElement.append("<");
1388         var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "html-tag-name");
1389         tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
1390         if (!isClosingTag && node.hasAttributes()) {
1391             var attributes = node.attributes();
1392             for (var i = 0; i < attributes.length; ++i) {
1393                 var attr = attributes[i];
1394                 tagElement.append(" ");
1395                 this._buildAttributeDOM(tagElement, attr.name, attr.value, node);
1396             }
1397         }
1398         tagElement.append(">");
1399         parentElement.append("\u200B");
1400
1401         if (this._showGoToArrow && node.nodeType() === Node.ELEMENT_NODE && willRenderCloseTagInline === isClosingTag) {
1402             let goToArrowElement = parentElement.appendChild(WI.createGoToArrowButton());
1403             goToArrowElement.title = WI.UIString("Reveal in Elements Tab");
1404             goToArrowElement.addEventListener("click", (event) => {
1405                 WI.domManager.inspectElement(this.representedObject.id);
1406             });
1407         }
1408     }
1409
1410     _nodeTitleInfo()
1411     {
1412         var node = this.representedObject;
1413         var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
1414
1415         function trimedNodeValue()
1416         {
1417             // Trim empty lines from the beginning and extra space at the end since most style and script tags begin with a newline
1418             // and end with a newline and indentation for the end tag.
1419             return node.nodeValue().replace(/^[\n\r]*/, "").replace(/\s*$/, "");
1420         }
1421
1422         switch (node.nodeType()) {
1423             case Node.DOCUMENT_FRAGMENT_NODE:
1424                 var fragmentElement = info.titleDOM.createChild("span", "html-fragment");
1425                 if (node.shadowRootType()) {
1426                     fragmentElement.textContent = WI.UIString("Shadow Content (%s)").format(WI.DOMTreeElement.shadowRootTypeDisplayName(node.shadowRootType()));
1427                     this.listItemElement.classList.add("shadow");
1428                 } else if (node.parentNode && node.parentNode.templateContent() === node) {
1429                     fragmentElement.textContent = WI.UIString("Template Content");
1430                     this.listItemElement.classList.add("template");
1431                 } else {
1432                     fragmentElement.textContent = WI.UIString("Document Fragment");
1433                     this.listItemElement.classList.add("fragment");
1434                 }
1435                 break;
1436
1437             case Node.ATTRIBUTE_NODE:
1438                 var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
1439                 this._buildAttributeDOM(info.titleDOM, node.name, value);
1440                 break;
1441
1442             case Node.ELEMENT_NODE:
1443                 if (node.isPseudoElement()) {
1444                     var pseudoElement = info.titleDOM.createChild("span", "html-pseudo-element");
1445                     pseudoElement.textContent = "::" + node.pseudoType();
1446                     info.titleDOM.appendChild(document.createTextNode("\u200B"));
1447                     info.hasChildren = false;
1448                     break;
1449                 }
1450
1451                 var tagName = node.nodeNameInCorrectCase();
1452                 if (this._elementCloseTag) {
1453                     this._buildTagDOM({
1454                         parentElement: info.titleDOM,
1455                         tagName,
1456                         isClosingTag: true,
1457                         isDistinctTreeElement: true,
1458                         willRenderCloseTagInline: false,
1459                     });
1460                     info.hasChildren = false;
1461                     break;
1462                 }
1463
1464                 var textChild = this._singleTextChild(node);
1465                 var showInlineText = textChild && textChild.nodeValue().length < WI.DOMTreeElement.MaximumInlineTextChildLength;
1466                 var showInlineEllipsis = !this.expanded && !showInlineText && (this.treeOutline.isXMLMimeType || !WI.DOMTreeElement.ForbiddenClosingTagElements.has(tagName));
1467
1468                 this._buildTagDOM({
1469                     parentElement: info.titleDOM,
1470                     tagName,
1471                     isClosingTag: false,
1472                     isDistinctTreeElement: false,
1473                     willRenderCloseTagInline: showInlineText || showInlineEllipsis,
1474                 });
1475
1476                 if (showInlineEllipsis) {
1477                     if (this.hasChildren) {
1478                         var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1479                         textNodeElement.textContent = ellipsis;
1480                         info.titleDOM.append("\u200B");
1481                     }
1482                     this._buildTagDOM({
1483                         parentElement: info.titleDOM,
1484                         tagName,
1485                         isClosingTag: true,
1486                         isDistinctTreeElement: false,
1487                         willRenderCloseTagInline: true,
1488                     });
1489                 }
1490
1491                 // If this element only has a single child that is a text node,
1492                 // just show that text and the closing tag inline rather than
1493                 // create a subtree for them
1494                 if (showInlineText) {
1495                     var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1496                     var nodeNameLowerCase = node.nodeName().toLowerCase();
1497
1498                     if (nodeNameLowerCase === "script")
1499                         textNodeElement.appendChild(WI.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/javascript"));
1500                     else if (nodeNameLowerCase === "style")
1501                         textNodeElement.appendChild(WI.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/css"));
1502                     else
1503                         textNodeElement.textContent = textChild.nodeValue();
1504
1505                     info.titleDOM.append("\u200B");
1506
1507                     this._buildTagDOM({
1508                         parentElement: info.titleDOM,
1509                         tagName,
1510                         isClosingTag: true,
1511                         isDistinctTreeElement: false,
1512                         willRenderCloseTagInline: true,
1513                     });
1514                     info.hasChildren = false;
1515                 }
1516                 break;
1517
1518             case Node.TEXT_NODE:
1519                 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
1520                     var newNode = info.titleDOM.createChild("span", "html-text-node large");
1521                     newNode.appendChild(WI.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/javascript"));
1522                 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
1523                     var newNode = info.titleDOM.createChild("span", "html-text-node large");
1524                     newNode.appendChild(WI.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/css"));
1525                 } else {
1526                     info.titleDOM.append("\"");
1527                     var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1528                     textNodeElement.textContent = node.nodeValue();
1529                     info.titleDOM.append("\"");
1530                 }
1531                 break;
1532
1533             case Node.COMMENT_NODE:
1534                 var commentElement = info.titleDOM.createChild("span", "html-comment");
1535                 commentElement.append("<!--" + node.nodeValue() + "-->");
1536                 break;
1537
1538             case Node.DOCUMENT_TYPE_NODE:
1539                 var docTypeElement = info.titleDOM.createChild("span", "html-doctype");
1540                 docTypeElement.append("<!DOCTYPE " + node.nodeName());
1541                 if (node.publicId) {
1542                     docTypeElement.append(" PUBLIC \"" + node.publicId + "\"");
1543                     if (node.systemId)
1544                         docTypeElement.append(" \"" + node.systemId + "\"");
1545                 } else if (node.systemId)
1546                     docTypeElement.append(" SYSTEM \"" + node.systemId + "\"");
1547
1548                 docTypeElement.append(">");
1549                 break;
1550
1551             case Node.CDATA_SECTION_NODE:
1552                 var cdataElement = info.titleDOM.createChild("span", "html-text-node");
1553                 cdataElement.append("<![CDATA[" + node.nodeValue() + "]]>");
1554                 break;
1555
1556             case Node.PROCESSING_INSTRUCTION_NODE:
1557                 var processingInstructionElement = info.titleDOM.createChild("span", "html-processing-instruction");
1558                 var data = node.nodeValue();
1559                 var dataString = data.length ? " " + data : "";
1560                 var title = "<?" + node.nodeNameInCorrectCase() + dataString + "?>";
1561                 processingInstructionElement.append(title);
1562                 break;
1563
1564             default:
1565                 info.titleDOM.append(node.nodeNameInCorrectCase().collapseWhitespace());
1566         }
1567
1568         return info;
1569     }
1570
1571     _singleTextChild(node)
1572     {
1573         if (!node || this._ignoreSingleTextChild)
1574             return null;
1575
1576         var firstChild = node.firstChild;
1577         if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE)
1578             return null;
1579
1580         if (node.hasShadowRoots())
1581             return null;
1582         if (node.templateContent())
1583             return null;
1584         if (node.hasPseudoElements())
1585             return null;
1586
1587         var sibling = firstChild.nextSibling;
1588         return sibling ? null : firstChild;
1589     }
1590
1591     _showInlineText(node)
1592     {
1593         if (node.nodeType() === Node.ELEMENT_NODE) {
1594             var textChild = this._singleTextChild(node);
1595             if (textChild && textChild.nodeValue().length < WI.DOMTreeElement.MaximumInlineTextChildLength)
1596                 return true;
1597         }
1598         return false;
1599     }
1600
1601     _hasVisibleChildren()
1602     {
1603         var node = this.representedObject;
1604
1605         if (this._showInlineText(node))
1606             return false;
1607
1608         if (node.hasChildNodes())
1609             return true;
1610         if (node.templateContent())
1611             return true;
1612         if (node.hasPseudoElements())
1613             return true;
1614
1615         return false;
1616     }
1617
1618     _visibleChildren()
1619     {
1620         var node = this.representedObject;
1621
1622         var visibleChildren = [];
1623
1624         var templateContent = node.templateContent();
1625         if (templateContent)
1626             visibleChildren.push(templateContent);
1627
1628         var beforePseudoElement = node.beforePseudoElement();
1629         if (beforePseudoElement)
1630             visibleChildren.push(beforePseudoElement);
1631
1632         if (node.childNodeCount && node.children)
1633             visibleChildren.pushAll(node.children);
1634
1635         var afterPseudoElement = node.afterPseudoElement();
1636         if (afterPseudoElement)
1637             visibleChildren.push(afterPseudoElement);
1638
1639         return visibleChildren;
1640     }
1641
1642     remove()
1643     {
1644         var parentElement = this.parent;
1645         if (!parentElement)
1646             return;
1647
1648         var self = this;
1649         function removeNodeCallback(error, removedNodeId)
1650         {
1651             if (error)
1652                 return;
1653
1654             if (!self.parent)
1655                 return;
1656
1657             parentElement.removeChild(self);
1658             parentElement.adjustCollapsedRange();
1659         }
1660
1661         this.representedObject.removeNode(removeNodeCallback);
1662     }
1663
1664     _insertAdjacentHTML(position, options = {})
1665     {
1666         let hasChildren = this.hasChildren;
1667
1668         let commitCallback = (value) => {
1669             this._ignoreSingleTextChild = false;
1670
1671             if (!value.length) {
1672                 if (!hasChildren) {
1673                     this._forceUpdateTitle = true;
1674                     this.hasChildren = false;
1675                     this._forceUpdateTitle = false;
1676                 }
1677                 return;
1678             }
1679
1680             this.representedObject.insertAdjacentHTML(position, value);
1681         };
1682
1683         if (position === "afterbegin" || position === "beforeend") {
1684             this._ignoreSingleTextChild = true;
1685             this.hasChildren = true;
1686             this.expand();
1687         }
1688
1689         this._startEditingAsHTML(commitCallback, {...options, position});
1690     }
1691
1692     _addHTML(event)
1693     {
1694         let options = {};
1695         switch (this.representedObject.nodeNameInCorrectCase()) {
1696         case "ul":
1697         case "ol":
1698             options.initialValue = "<li></li>";
1699             options.startPosition = 4;
1700             break;
1701         case "table":
1702         case "thead":
1703         case "tbody":
1704         case "tfoot":
1705             options.initialValue = "<tr></tr>";
1706             options.startPosition = 4;
1707             break;
1708         case "tr":
1709             options.initializing = "<td></td>";
1710             options.startPosition = 4;
1711             break;
1712         }
1713         this._insertAdjacentHTML("beforeend", options);
1714     }
1715
1716     _addPreviousSibling(event)
1717     {
1718         let options = {};
1719         let nodeName = this.representedObject.nodeNameInCorrectCase();
1720         if (nodeName === "li" || nodeName === "tr" || nodeName === "th" || nodeName === "td") {
1721             options.initialValue = `<${nodeName}></${nodeName}>`;
1722             options.startPosition = nodeName.length + 2;
1723         }
1724         this._insertAdjacentHTML("beforebegin", options);
1725     }
1726
1727     _addNextSibling(event)
1728     {
1729         let options = {};
1730         let nodeName = this.representedObject.nodeNameInCorrectCase();
1731         if (nodeName === "li" || nodeName === "tr" || nodeName === "th" || nodeName === "td") {
1732             options.initialValue = `<${nodeName}></${nodeName}>`;
1733             options.startPosition = nodeName.length + 2;
1734         }
1735         this._insertAdjacentHTML("afterend", options);
1736     }
1737
1738     _editAsHTML()
1739     {
1740         var treeOutline = this.treeOutline;
1741         var node = this.representedObject;
1742         var parentNode = node.parentNode;
1743         var index = node.index;
1744         var wasExpanded = this.expanded;
1745
1746         function selectNode(error, nodeId)
1747         {
1748             if (error)
1749                 return;
1750
1751             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1752             treeOutline._updateModifiedNodes();
1753
1754             var newNode = parentNode ? parentNode.children[index] || parentNode : null;
1755             if (!newNode)
1756                 return;
1757
1758             treeOutline.selectDOMNode(newNode, true);
1759
1760             if (wasExpanded) {
1761                 var newTreeItem = treeOutline.findTreeElement(newNode);
1762                 if (newTreeItem)
1763                     newTreeItem.expand();
1764             }
1765         }
1766
1767         function commitChange(value)
1768         {
1769             node.setOuterHTML(value, selectNode);
1770         }
1771
1772         node.getOuterHTML((error, initialValue) => {
1773             if (error)
1774                 return;
1775
1776             this._startEditingAsHTML(commitChange, {
1777                 initialValue,
1778                 hideExistingElements: true,
1779             });
1780         });
1781     }
1782
1783     _copyHTML()
1784     {
1785         this.representedObject.copyNode();
1786     }
1787
1788     _highlightSearchResults()
1789     {
1790         if (!this.title || !this._searchQuery || !this._searchHighlightsVisible)
1791             return;
1792
1793         if (this._highlightResult) {
1794             this._updateSearchHighlight(true);
1795             return;
1796         }
1797
1798         var text = this.title.textContent;
1799         let searchRegex = WI.SearchUtilities.regExpForString(this._searchQuery, WI.SearchUtilities.defaultSettings);
1800
1801         var match = searchRegex.exec(text);
1802         var matchRanges = [];
1803         while (match) {
1804             matchRanges.push({offset: match.index, length: match[0].length});
1805             match = searchRegex.exec(text);
1806         }
1807
1808         // Fall back for XPath, etc. matches.
1809         if (!matchRanges.length)
1810             matchRanges.push({offset: 0, length: text.length});
1811
1812         this._highlightResult = [];
1813         WI.highlightRangesWithStyleClass(this.title, matchRanges, WI.DOMTreeElement.SearchHighlightStyleClassName, this._highlightResult);
1814     }
1815
1816     _createModifiedAnimation(key, value, element)
1817     {
1818         let existing = this._recentlyModifiedAttributes.get(key);
1819         if (!existing)
1820             return;
1821
1822         if (existing.element) {
1823             if (existing.listener)
1824                 existing.element.removeEventListener("animationend", existing.listener);
1825
1826             existing.element.classList.remove("node-state-changed");
1827             existing.element.style.removeProperty("animation-delay");
1828         }
1829
1830         existing.listener = (event) => {
1831             element.classList.remove("node-state-changed");
1832             element.style.removeProperty("animation-delay");
1833
1834             this._recentlyModifiedAttributes.delete(key);
1835         };
1836
1837         element.classList.remove("node-state-changed");
1838         element.style.removeProperty("animation-delay");
1839
1840         if (existing.value === value)
1841             element.style.setProperty("animation-delay", "-" + (performance.now() - existing.timestamp) + "ms");
1842         else
1843             existing.timestamp = performance.now();
1844
1845         existing.value = value;
1846         existing.element = element;
1847
1848         element.addEventListener("animationend", existing.listener, {once: true});
1849         element.classList.add("node-state-changed");
1850     }
1851
1852     get isNodeHidden()
1853     {
1854         let classes = this.representedObject.getAttribute("class");
1855         return classes && classes.includes(WI.DOMTreeElement.HideElementStyleSheetIdOrClassName);
1856     }
1857
1858     _updatePseudoClassIndicator()
1859     {
1860         if (!this.listItemElement || this._elementCloseTag)
1861             return;
1862
1863         if (this.representedObject.enabledPseudoClasses.length) {
1864             if (!this._pseudoClassIndicatorElement) {
1865                 this._pseudoClassIndicatorElement = document.createElement("div");
1866                 this._pseudoClassIndicatorElement.classList.add("pseudo-class-indicator");
1867             }
1868             this.listItemElement.insertBefore(this._pseudoClassIndicatorElement, this.listItemElement.firstChild);
1869         } else {
1870             if (this._pseudoClassIndicatorElement) {
1871                 this._pseudoClassIndicatorElement.remove();
1872                 this._pseudoClassIndicatorElement = null;
1873             }
1874         }
1875     }
1876
1877     handleEvent(event)
1878     {
1879         if (event.type === "dragstart" && this._editing)
1880             event.preventDefault();
1881     }
1882
1883     _subtreeBreakpointChanged(treeElement)
1884     {
1885         if (treeElement.hasBreakpoint) {
1886             if (!this._subtreeBreakpointTreeElements)
1887                 this._subtreeBreakpointTreeElements = new Set;
1888             this._subtreeBreakpointTreeElements.add(treeElement);
1889         } else {
1890             this._subtreeBreakpointTreeElements.delete(treeElement);
1891             if (!this._subtreeBreakpointTreeElements.size)
1892                 this._subtreeBreakpointTreeElements = null;
1893         }
1894
1895         this._updateBreakpointStatus();
1896     }
1897
1898     _updateBreakpointStatus()
1899     {
1900         let listItemElement = this.listItemElement;
1901         if (!listItemElement)
1902             return;
1903
1904         let hasBreakpoint = this._breakpointStatus !== WI.DOMTreeElement.BreakpointStatus.None;
1905         let hasSubtreeBreakpoints = this._subtreeBreakpointTreeElements && this._subtreeBreakpointTreeElements.size;
1906
1907         if (!hasBreakpoint && !hasSubtreeBreakpoints) {
1908             if (this._statusImageElement)
1909                 this._statusImageElement.remove();
1910             return;
1911         }
1912
1913         if (!this._statusImageElement) {
1914             this._statusImageElement = WI.ImageUtilities.useSVGSymbol("Images/DOMBreakpoint.svg", "status-image");
1915             this._statusImageElement.classList.add("breakpoint");
1916             this._statusImageElement.addEventListener("click", this._statusImageClicked.bind(this));
1917             this._statusImageElement.addEventListener("contextmenu", this._statusImageContextmenu.bind(this));
1918             this._statusImageElement.addEventListener("mousedown", (event) => { event.stopPropagation(); });
1919         }
1920
1921         this._statusImageElement.classList.toggle("subtree", !hasBreakpoint && hasSubtreeBreakpoints);
1922
1923         this.listItemElement.insertBefore(this._statusImageElement, this.listItemElement.firstChild);
1924
1925         let disabled = this._breakpointStatus === WI.DOMTreeElement.BreakpointStatus.DisabledBreakpoint;
1926         this._statusImageElement.classList.toggle("disabled", disabled);
1927     }
1928
1929     _statusImageClicked(event)
1930     {
1931         if (this._breakpointStatus === WI.DOMTreeElement.BreakpointStatus.None)
1932             return;
1933
1934         if (event.button !== 0 || event.ctrlKey)
1935             return;
1936
1937         let breakpoints = WI.domDebuggerManager.domBreakpointsForNode(this.representedObject);
1938         if (!breakpoints || !breakpoints.length)
1939             return;
1940
1941         let shouldEnable = breakpoints.some((breakpoint) => breakpoint.disabled);
1942         breakpoints.forEach((breakpoint) => { breakpoint.disabled = !shouldEnable });
1943     }
1944
1945     _statusImageContextmenu(event)
1946     {
1947         if (!this.hasBreakpoint)
1948             return;
1949
1950         let contextMenu = WI.ContextMenu.createFromEvent(event);
1951
1952         WI.appendContextMenuItemsForDOMNodeBreakpoints(contextMenu, this.representedObject, {
1953             revealDescendantBreakpointsMenuItemHandler: this.bindRevealDescendantBreakpointsMenuItemHandler(),
1954         });
1955     }
1956
1957     _highlightAnimationEnd()
1958     {
1959         let listItemElement = this.listItemElement;
1960         if (!listItemElement)
1961             return;
1962
1963         listItemElement.removeEventListener("animationend", this._boundHighlightAnimationEnd);
1964         listItemElement.classList.remove(WI.DOMTreeElement.HighlightStyleClassName);
1965
1966         this._animatingHighlight = false;
1967     }
1968 };
1969
1970 WI.DOMTreeElement.InitialChildrenLimit = 500;
1971 WI.DOMTreeElement.MaximumInlineTextChildLength = 80;
1972
1973 // A union of HTML4 and HTML5-Draft elements that explicitly
1974 // or implicitly (for HTML5) forbid the closing tag.
1975 WI.DOMTreeElement.ForbiddenClosingTagElements = new Set([
1976     "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
1977     "hr", "img", "input", "keygen", "link", "meta", "param", "source",
1978     "wbr", "track", "menuitem"
1979 ]);
1980
1981 // These tags we do not allow editing their tag name.
1982 WI.DOMTreeElement.EditTagBlacklist = new Set([
1983     "html", "head", "body"
1984 ]);
1985
1986 WI.DOMTreeElement.BreakpointStatus = {
1987     None: Symbol("none"),
1988     Breakpoint: Symbol("breakpoint"),
1989     DisabledBreakpoint: Symbol("disabled-breakpoint"),
1990 };
1991
1992 WI.DOMTreeElement.HighlightStyleClassName = "highlight";
1993 WI.DOMTreeElement.SearchHighlightStyleClassName = "search-highlight";
1994 WI.DOMTreeElement.BouncyHighlightStyleClassName = "bouncy-highlight";
1995 WI.DOMTreeElement.HideElementStyleSheetIdOrClassName = "__WebInspectorHideElement__";