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