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