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
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
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.
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.
31 WI.DOMTreeElement = class DOMTreeElement extends WI.TreeElement
33 constructor(node, elementCloseTag)
37 this._elementCloseTag = elementCloseTag;
38 this.hasChildren = !elementCloseTag && this._hasVisibleChildren();
40 if (this.representedObject.nodeType() === Node.ELEMENT_NODE && !elementCloseTag)
41 this._canAddAttributes = true;
42 this._searchQuery = null;
43 this._expandedChildrenLimit = WI.DOMTreeElement.InitialChildrenLimit;
44 this._breakpointStatus = WI.DOMTreeElement.BreakpointStatus.None;
45 this._animatingHighlight = false;
46 this._shouldHighlightAfterReveal = false;
47 this._boundHighlightAnimationEnd = this._highlightAnimationEnd.bind(this);
48 this._subtreeBreakpointTreeElements = null;
50 this._showGoToArrow = false;
51 this._highlightedAttributes = new Set;
52 this._recentlyModifiedAttributes = new Map;
53 this._closeTagTreeElement = null;
55 node.addEventListener(WI.DOMNode.Event.EnabledPseudoClassesChanged, this._updatePseudoClassIndicator, this);
57 this._ignoreSingleTextChild = false;
58 this._forceUpdateTitle = false;
63 static shadowRootTypeDisplayName(type)
66 case WI.DOMNode.ShadowRootType.UserAgent:
67 return WI.UIString("User Agent");
68 case WI.DOMNode.ShadowRootType.Open:
69 return WI.UIString("Open");
70 case WI.DOMNode.ShadowRootType.Closed:
71 return WI.UIString("Closed");
79 return this._breakpointStatus !== WI.DOMTreeElement.BreakpointStatus.None || (this._subtreeBreakpointTreeElements && this._subtreeBreakpointTreeElements.size);
82 get breakpointStatus()
84 return this._breakpointStatus;
87 set breakpointStatus(status)
89 if (this._breakpointStatus === status)
93 if (this._breakpointStatus === WI.DOMTreeElement.BreakpointStatus.None)
95 else if (status === WI.DOMTreeElement.BreakpointStatus.None)
98 this._breakpointStatus = status;
99 this._updateBreakpointStatus();
104 let parentElement = this.parent;
105 while (parentElement && !parentElement.root) {
106 parentElement._subtreeBreakpointChanged(this);
107 parentElement = parentElement.parent;
111 bindRevealDescendantBreakpointsMenuItemHandler()
113 if (!this._subtreeBreakpointTreeElements || !this._subtreeBreakpointTreeElements.size)
116 let subtreeBreakpointTreeElements = Array.from(this._subtreeBreakpointTreeElements);
118 for (let subtreeBreakpointTreeElement of subtreeBreakpointTreeElements)
119 subtreeBreakpointTreeElement.reveal();
123 get closeTagTreeElement() { return this._closeTagTreeElement; }
127 if (this._animatingHighlight)
130 this._shouldHighlightAfterReveal = true;
136 return this._elementCloseTag;
139 highlightSearchResults(searchQuery)
141 if (this._searchQuery !== searchQuery) {
142 this._updateSearchHighlight(false);
143 this._highlightResult = undefined; // A new search query.
146 this._searchQuery = searchQuery;
147 this._searchHighlightsVisible = true;
148 this.updateTitle(true);
151 hideSearchHighlights()
153 this._searchHighlightsVisible = false;
154 this._updateSearchHighlight(false);
157 emphasizeSearchHighlight()
159 var highlightElement = this.title.querySelector("." + WI.DOMTreeElement.SearchHighlightStyleClassName);
160 console.assert(highlightElement);
161 if (!highlightElement)
164 if (this._bouncyHighlightElement)
165 this._bouncyHighlightElement.remove();
167 this._bouncyHighlightElement = document.createElement("div");
168 this._bouncyHighlightElement.className = WI.DOMTreeElement.BouncyHighlightStyleClassName;
169 this._bouncyHighlightElement.textContent = highlightElement.textContent;
171 // Position and show the bouncy highlight adjusting the coordinates to be inside the TreeOutline's space.
172 var highlightElementRect = highlightElement.getBoundingClientRect();
173 var treeOutlineRect = this.treeOutline.element.getBoundingClientRect();
174 this._bouncyHighlightElement.style.top = (highlightElementRect.top - treeOutlineRect.top) + "px";
175 this._bouncyHighlightElement.style.left = (highlightElementRect.left - treeOutlineRect.left) + "px";
176 this.title.appendChild(this._bouncyHighlightElement);
178 function animationEnded()
180 if (!this._bouncyHighlightElement)
183 this._bouncyHighlightElement.remove();
184 this._bouncyHighlightElement = null;
187 this._bouncyHighlightElement.addEventListener("animationend", animationEnded.bind(this));
190 _updateSearchHighlight(show)
192 if (!this._highlightResult)
195 function updateEntryShow(entry)
197 switch (entry.type) {
199 entry.parent.insertBefore(entry.node, entry.nextSibling);
202 entry.node.textContent = entry.newText;
207 function updateEntryHide(entry)
209 switch (entry.type) {
214 entry.node.textContent = entry.oldText;
219 var updater = show ? updateEntryShow : updateEntryHide;
221 for (var i = 0, size = this._highlightResult.length; i < size; ++i)
222 updater(this._highlightResult[i]);
227 return this._hovered;
232 if (this._hovered === value)
235 this._hovered = value;
237 if (this.listItemElement) {
238 this.listItemElement.classList.toggle("hovered", this._hovered);
239 this.updateSelectionArea();
245 let node = this.representedObject;
247 if (node.isShadowRoot() || node.isInUserAgentShadowTree())
250 if (node.isPseudoElement())
253 return this.treeOutline.editable;
256 get expandedChildrenLimit()
258 return this._expandedChildrenLimit;
261 set expandedChildrenLimit(x)
263 if (this._expandedChildrenLimit === x)
266 this._expandedChildrenLimit = x;
267 if (this.treeOutline && !this._updateChildrenInProgress)
268 this._updateChildren(true);
271 get expandedChildCount()
273 var count = this.children.length;
274 if (count && this.children[count - 1]._elementCloseTag)
276 if (count && this.children[count - 1].expandAllButton)
283 if (this._showGoToArrow === x)
286 this._showGoToArrow = x;
291 attributeDidChange(name)
293 if (this._recentlyModifiedAttributes.has(name))
296 this._recentlyModifiedAttributes.set(name, {
304 highlightAttribute(name)
306 this._highlightedAttributes.add(name);
311 console.assert(!this._elementCloseTag);
312 if (this._elementCloseTag)
315 var index = this._visibleChildren().indexOf(node);
319 if (index >= this.expandedChildrenLimit) {
320 this._expandedChildrenLimit = index + 1;
321 this._updateChildren(true);
324 return this.children[index];
327 toggleElementVisibility(forceHidden)
329 let effectiveNode = this.representedObject;
330 if (effectiveNode.isPseudoElement()) {
331 effectiveNode = effectiveNode.parentNode;
332 console.assert(effectiveNode);
337 if (effectiveNode.nodeType() !== Node.ELEMENT_NODE)
340 function inspectedPage_node_injectStyleAndToggleClass(hiddenClassName, force) {
341 let styleElement = document.getElementById(hiddenClassName);
343 styleElement = document.createElement("style");
344 styleElement.id = hiddenClassName;
345 styleElement.textContent = `.${hiddenClassName} { visibility: hidden !important; }`;
346 document.head.appendChild(styleElement);
349 this.classList.toggle(hiddenClassName, force);
352 WI.RemoteObject.resolveNode(effectiveNode).then((object) => {
353 object.callFunction(inspectedPage_node_injectStyleAndToggleClass, [WI.DOMTreeElement.HideElementStyleSheetIdOrClassName, forceHidden], false);
358 _createTooltipForNode()
360 var node = this.representedObject;
361 if (!node.nodeName() || node.nodeName().toLowerCase() !== "img")
364 function setTooltip(error, result, wasThrown)
366 if (error || wasThrown || !result || result.type !== "string")
370 var properties = JSON.parse(result.description);
371 var offsetWidth = properties[0];
372 var offsetHeight = properties[1];
373 var naturalWidth = properties[2];
374 var naturalHeight = properties[3];
375 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
376 this.tooltip = WI.UIString("%d \xd7 %d pixels").format(offsetWidth, offsetHeight);
378 this.tooltip = WI.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)").format(offsetWidth, offsetHeight, naturalWidth, naturalHeight);
384 WI.RemoteObject.resolveNode(node).then((object) => {
385 function inspectedPage_node_dimensions() {
386 return "[" + this.offsetWidth + "," + this.offsetHeight + "," + this.naturalWidth + "," + this.naturalHeight + "]";
389 object.callFunction(inspectedPage_node_dimensions, undefined, false, setTooltip.bind(this));
394 updateSelectionArea()
396 let listItemElement = this.listItemElement;
397 if (!listItemElement)
400 // If there's no reason to have a selection area, remove the DOM element.
401 let indicatesTreeOutlineState = this.treeOutline && (this.treeOutline.dragOverTreeElement === this || this.selected || this._animatingHighlight);
402 if (!this.hovered && !indicatesTreeOutlineState) {
403 if (this._selectionElement) {
404 this._selectionElement.remove();
405 this._selectionElement = null;
411 if (!this._selectionElement) {
412 this._selectionElement = document.createElement("div");
413 this._selectionElement.className = "selection-area";
414 listItemElement.insertBefore(this._selectionElement, listItemElement.firstChild);
417 this._selectionElement.style.height = listItemElement.offsetHeight + "px";
423 this.listItemElement.classList.add("hovered");
428 this.listItemElement.draggable = true;
429 this.listItemElement.addEventListener("dragstart", this);
435 if (this.children.length || !this._hasVisibleChildren() || this._elementCloseTag)
438 this.updateChildren();
443 this.representedObject.getSubtree(-1, super.expandRecursively.bind(this, Number.MAX_VALUE));
446 updateChildren(fullRefresh)
448 if (this._elementCloseTag)
451 this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh));
454 insertChildElement(child, index, closingTag)
456 var newElement = new WI.DOMTreeElement(child, closingTag);
457 newElement.selectable = this.treeOutline.selectable;
458 this.insertChild(newElement, index);
462 moveChild(child, targetIndex)
464 // No move needed if the child is already in the right place.
465 if (this.children[targetIndex] === child)
468 var originalSelectedChild = this.treeOutline.selectedTreeElement;
470 this.removeChild(child);
471 this.insertChild(child, targetIndex);
473 if (originalSelectedChild !== this.treeOutline.selectedTreeElement)
474 originalSelectedChild.select();
477 _updateChildren(fullRefresh)
479 if (this._updateChildrenInProgress || !this.treeOutline._visible)
482 this._closeTagTreeElement = null;
483 this._updateChildrenInProgress = true;
485 var node = this.representedObject;
486 var selectedNode = this.treeOutline.selectedDOMNode();
487 var originalScrollTop = 0;
489 var hasVisibleChildren = this._hasVisibleChildren();
491 if (fullRefresh || !hasVisibleChildren) {
492 var treeOutlineContainerElement = this.treeOutline.element.parentNode;
493 originalScrollTop = treeOutlineContainerElement.scrollTop;
494 var selectedTreeElement = this.treeOutline.selectedTreeElement;
495 if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
497 this.removeChildren();
499 // No longer have children.
500 if (!hasVisibleChildren) {
501 this.hasChildren = false;
503 this._updateChildrenInProgress = false;
508 // We now have children.
509 if (!this.hasChildren) {
510 this.hasChildren = true;
514 // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
515 // Keep a list of existing tree elements for nodes that we can use later.
516 var existingChildTreeElements = new Map;
517 for (var i = this.children.length - 1; i >= 0; --i) {
518 var currentChildTreeElement = this.children[i];
519 var currentNode = currentChildTreeElement.representedObject;
520 var currentParentNode = currentNode.parentNode;
521 if (currentParentNode === node) {
522 existingChildTreeElements.set(currentNode, currentChildTreeElement);
526 this.removeChildAtIndex(i);
529 // Move / create TreeElements for our visible children.
530 var elementToSelect = null;
531 var visibleChildren = this._visibleChildren();
532 for (var i = 0; i < visibleChildren.length && i < this.expandedChildrenLimit; ++i) {
533 var childNode = visibleChildren[i];
535 // Already have a tree element for this child, just move it.
536 var existingChildTreeElement = existingChildTreeElements.get(childNode);
537 if (existingChildTreeElement) {
538 this.moveChild(existingChildTreeElement, i);
542 // No existing tree element for this child. Insert a new element.
543 var newChildTreeElement = this.insertChildElement(childNode, i);
546 if (childNode === selectedNode)
547 elementToSelect = newChildTreeElement;
548 if (this.expandedChildCount > this.expandedChildrenLimit)
549 this.expandedChildrenLimit++;
552 // Update expand all children button.
553 this.adjustCollapsedRange();
555 // Insert closing tag tree element.
556 var lastChild = this.children.lastValue;
557 if (node.nodeType() === Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
558 this._closeTagTreeElement = this.insertChildElement(this.representedObject, this.children.length, true);
560 // We want to restore the original selection and tree scroll position after a full refresh, if possible.
561 if (fullRefresh && elementToSelect) {
562 elementToSelect.select();
563 if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
564 treeOutlineContainerElement.scrollTop = originalScrollTop;
567 this._updateChildrenInProgress = false;
570 adjustCollapsedRange()
572 // Ensure precondition: only the tree elements for node children are found in the tree
573 // (not the Expand All button or the closing tag).
574 if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
575 this.removeChild(this.expandAllButtonElement.__treeElement);
577 if (!this._hasVisibleChildren())
580 var visibleChildren = this._visibleChildren();
581 var totalChildrenCount = visibleChildren.length;
583 // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
584 for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, totalChildrenCount); i < limit; ++i)
585 this.insertChildElement(visibleChildren[i], i);
587 var expandedChildCount = this.expandedChildCount;
588 if (totalChildrenCount > this.expandedChildCount) {
589 var targetButtonIndex = expandedChildCount;
590 if (!this.expandAllButtonElement) {
591 var button = document.createElement("button");
592 button.className = "show-all-nodes";
595 var item = new WI.TreeElement(button, null, false);
596 item.selectable = false;
597 item.expandAllButton = true;
599 this.insertChild(item, targetButtonIndex);
600 this.expandAllButtonElement = button;
601 this.expandAllButtonElement.__treeElement = item;
602 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
603 } else if (!this.expandAllButtonElement.__treeElement.parent)
604 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
606 this.expandAllButtonElement.textContent = WI.UIString("Show All Nodes (%d More)").format(totalChildrenCount - expandedChildCount);
607 } else if (this.expandAllButtonElement)
608 this.expandAllButtonElement = null;
611 handleLoadAllChildren()
613 var visibleChildren = this._visibleChildren();
614 this.expandedChildrenLimit = Math.max(visibleChildren.length, this.expandedChildrenLimit + WI.DOMTreeElement.InitialChildrenLimit);
619 if (this._elementCloseTag)
622 if (!this.listItemElement)
627 for (let treeElement of this.children)
628 treeElement.updateSelectionArea();
633 if (this._elementCloseTag)
641 let listItemElement = this.listItemElement;
642 if (!listItemElement)
645 let tagSpans = listItemElement.getElementsByClassName("html-tag-name");
647 tagSpans[0].scrollIntoViewIfNeeded(false);
649 listItemElement.scrollIntoViewIfNeeded(false);
651 if (!this._shouldHighlightAfterReveal)
654 this._shouldHighlightAfterReveal = false;
655 this._animatingHighlight = true;
657 this.updateSelectionArea();
659 listItemElement.addEventListener("animationend", this._boundHighlightAnimationEnd);
660 listItemElement.classList.add(WI.DOMTreeElement.HighlightStyleClassName);
668 // On Enter or Return start editing the first attribute
669 // or create a new attribute on the selected element.
670 if (this.treeOutline.editing)
673 this._startEditing();
675 // prevent a newline from being immediately inserted
679 canSelectOnMouseDown(event)
684 // Prevent selecting the nearest word on double click.
685 if (event.detail >= 2) {
686 event.preventDefault();
698 if (this._editing || this._elementCloseTag)
701 if (this._startEditingTarget(event.target))
704 if (this.hasChildren && !this.expanded)
708 _insertInLastAttributePosition(tag, node)
710 if (tag.getElementsByClassName("html-attribute").length > 0)
711 tag.insertBefore(node, tag.lastChild);
713 var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
714 tag.textContent = "";
715 tag.append("<" + nodeName, node, ">");
718 this.updateSelectionArea();
721 _startEditingTarget(eventTarget)
723 if (this.treeOutline.selectedDOMNode() !== this.representedObject)
726 if (this.representedObject.isShadowRoot() || this.representedObject.isInUserAgentShadowTree())
729 if (this.representedObject.isPseudoElement())
732 if (this.representedObject.nodeType() !== Node.ELEMENT_NODE && this.representedObject.nodeType() !== Node.TEXT_NODE)
735 var textNode = eventTarget.closest(".html-text-node");
737 return this._startEditingTextNode(textNode);
739 var attribute = eventTarget.closest(".html-attribute");
741 return this._startEditingAttribute(attribute, eventTarget);
743 var tagName = eventTarget.closest(".html-tag-name");
745 return this._startEditingTagName(tagName);
750 populateDOMNodeContextMenu(contextMenu, subMenus, event)
752 let attributeNode = event.target.closest(".html-attribute");
753 let textNode = event.target.closest(".html-text-node");
755 let attributeName = null;
757 let attributeNameElement = attributeNode.getElementsByClassName("html-attribute-name")[0];
758 if (attributeNameElement)
759 attributeName = attributeNameElement.textContent.trim();
762 if (event.target && event.target.tagName === "A")
763 WI.appendContextMenuItemsForURL(contextMenu, event.target.href, {frame: this.representedObject.frame});
765 contextMenu.appendSeparator();
767 let isEditableNode = this.representedObject.nodeType() === Node.ELEMENT_NODE && this.editable;
768 let isNonShadowEditable = !this.representedObject.isInUserAgentShadowTree() && isEditableNode;
769 let alreadyEditingHTML = this._htmlEditElement && WI.isBeingEdited(this._htmlEditElement);
771 if (isEditableNode) {
772 if (!DOMTreeElement.ForbiddenClosingTagElements.has(this.representedObject.nodeNameInCorrectCase())) {
773 subMenus.add.appendItem(WI.UIString("Child", "A submenu item of 'Add' to append DOM nodes to the selected DOM node"), () => {
775 }, alreadyEditingHTML);
778 subMenus.add.appendItem(WI.UIString("Previous Sibling", "A submenu item of 'Add' to add DOM nodes before the selected DOM node"), () => {
779 this._addPreviousSibling();
780 }, alreadyEditingHTML);
782 subMenus.add.appendItem(WI.UIString("Next Sibling", "A submenu item of 'Add' to add DOM nodes after the selected DOM node"), () => {
783 this._addNextSibling();
784 }, alreadyEditingHTML);
787 if (isNonShadowEditable) {
788 subMenus.add.appendItem(WI.UIString("Attribute"), () => {
789 this._addNewAttribute();
794 subMenus.edit.appendItem(WI.UIString("HTML"), () => {
796 }, alreadyEditingHTML);
799 if (isNonShadowEditable) {
801 subMenus.edit.appendItem(WI.UIString("Attribute"), () => {
802 this._startEditingAttribute(attributeNode, event.target);
803 }, WI.isBeingEdited(attributeNode));
806 if (!DOMTreeElement.EditTagBlacklist.has(this.representedObject.nodeNameInCorrectCase())) {
807 let tagNameNode = event.target.closest(".html-tag-name");
809 subMenus.edit.appendItem(WI.UIString("Tag", "A submenu item of 'Edit' to change DOM element's tag name"), () => {
810 this._startEditingTagName(tagNameNode);
811 }, WI.isBeingEdited(tagNameNode));
815 if (textNode && this.editable) {
816 subMenus.edit.appendItem(WI.UIString("Text"), () => {
817 this._startEditingTextNode(textNode);
818 }, WI.isBeingEdited(textNode));
821 if (!this.representedObject.isPseudoElement()) {
822 subMenus.copy.appendItem(WI.UIString("HTML"), () => {
828 subMenus.copy.appendItem(WI.UIString("Attribute"), () => {
829 let text = attributeName;
830 let attributeValue = this.representedObject.getAttribute(attributeName);
832 text += "=\"" + attributeValue.replace(/"/g, "\\\"") + "\"";
833 InspectorFrontendHost.copyText(text);
837 if (textNode && textNode.textContent.length) {
838 subMenus.copy.appendItem(WI.UIString("Text"), () => {
839 InspectorFrontendHost.copyText(textNode.textContent);
843 if (this.editable && (!this.selected || this.treeOutline.selectedTreeElements.length === 1)) {
844 subMenus.delete.appendItem(WI.UIString("Node"), () => {
849 if (attributeName && isNonShadowEditable) {
850 subMenus.delete.appendItem(WI.UIString("Attribute"), () => {
851 this.representedObject.removeAttribute(attributeName);
855 for (let subMenu of Object.values(subMenus))
856 contextMenu.pushItem(subMenu);
858 if (this.treeOutline.editable) {
859 if (this.selected && this.treeOutline && this.treeOutline.selectedTreeElements.length > 1) {
860 let forceHidden = !this.treeOutline.selectedTreeElements.every((treeElement) => treeElement.isNodeHidden);
861 let label = forceHidden ? WI.UIString("Hide Elements") : WI.UIString("Show Elements");
862 contextMenu.appendItem(label, () => {
863 this.treeOutline.toggleSelectedElementsVisibility(forceHidden);
865 } else if (isEditableNode) {
866 contextMenu.appendItem(WI.UIString("Toggle Visibility"), () => {
867 this.toggleElementVisibility();
875 if (this.treeOutline.selectedDOMNode() !== this.representedObject)
881 var listItem = this.listItemElement;
883 if (this._canAddAttributes) {
884 var attribute = listItem.getElementsByClassName("html-attribute")[0];
886 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("html-attribute-value")[0]);
888 return this._addNewAttribute();
891 if (this.representedObject.nodeType() === Node.TEXT_NODE) {
892 var textNode = listItem.getElementsByClassName("html-text-node")[0];
894 return this._startEditingTextNode(textNode);
901 // Cannot just convert the textual html into an element without
902 // a parent node. Use a temporary span container for the HTML.
903 var container = document.createElement("span");
904 this._buildAttributeDOM(container, " ", "");
905 var attr = container.firstChild;
906 attr.style.marginLeft = "2px"; // overrides the .editing margin rule
907 attr.style.marginRight = "2px"; // overrides the .editing margin rule
909 var tag = this.listItemElement.getElementsByClassName("html-tag")[0];
910 this._insertInLastAttributePosition(tag, attr);
911 return this._startEditingAttribute(attr, attr);
914 _triggerEditAttribute(attributeName)
916 var attributeElements = this.listItemElement.getElementsByClassName("html-attribute-name");
917 for (var i = 0, len = attributeElements.length; i < len; ++i) {
918 if (attributeElements[i].textContent === attributeName) {
919 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
920 if (elem.nodeType !== Node.ELEMENT_NODE)
923 if (elem.classList.contains("html-attribute-value"))
924 return this._startEditingAttribute(elem.parentNode, elem);
930 _startEditingAttribute(attribute, elementForSelection)
932 if (WI.isBeingEdited(attribute))
935 var attributeNameElement = attribute.getElementsByClassName("html-attribute-name")[0];
936 if (!attributeNameElement)
939 var attributeName = attributeNameElement.textContent;
941 function removeZeroWidthSpaceRecursive(node)
943 if (node.nodeType === Node.TEXT_NODE) {
944 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
948 if (node.nodeType !== Node.ELEMENT_NODE)
951 for (var child = node.firstChild; child; child = child.nextSibling)
952 removeZeroWidthSpaceRecursive(child);
955 // Remove zero-width spaces that were added by nodeTitleInfo.
956 removeZeroWidthSpaceRecursive(attribute);
958 var config = new WI.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
959 config.setNumberCommitHandler(this._attributeNumberEditingCommitted.bind(this));
960 this._editing = WI.startEditing(attribute, config);
962 window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
967 _startEditingTextNode(textNode)
969 if (WI.isBeingEdited(textNode))
972 var config = new WI.EditingConfig(this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
973 config.spellcheck = true;
974 this._editing = WI.startEditing(textNode, config);
975 window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
980 _startEditingTagName(tagNameElement)
982 if (!tagNameElement) {
983 tagNameElement = this.listItemElement.getElementsByClassName("html-tag-name")[0];
988 var tagName = tagNameElement.textContent;
989 if (WI.DOMTreeElement.EditTagBlacklist.has(tagName.toLowerCase()))
992 if (WI.isBeingEdited(tagNameElement))
995 let closingTagElement = this._distinctClosingTagElement();
996 let originalClosingTagTextContent = closingTagElement ? closingTagElement.textContent : "";
998 function keyupListener(event)
1000 if (closingTagElement)
1001 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
1004 function editingComitted(element, newTagName)
1006 tagNameElement.removeEventListener("keyup", keyupListener, false);
1007 this._tagNameEditingCommitted.apply(this, arguments);
1010 function editingCancelled()
1012 if (closingTagElement)
1013 closingTagElement.textContent = originalClosingTagTextContent;
1015 tagNameElement.removeEventListener("keyup", keyupListener, false);
1016 this._editingCancelled.apply(this, arguments);
1019 tagNameElement.addEventListener("keyup", keyupListener, false);
1021 var config = new WI.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName);
1022 this._editing = WI.startEditing(tagNameElement, config);
1023 window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
1027 _startEditingAsHTML(commitCallback, options = {})
1029 if (this._htmlEditElement && WI.isBeingEdited(this._htmlEditElement))
1032 if (options.hideExistingElements) {
1033 let child = this.listItemElement.firstChild;
1035 child.style.display = "none";
1036 child = child.nextSibling;
1038 if (this._childrenListNode)
1039 this._childrenListNode.style.display = "none";
1042 let positionInside = options.position === "afterbegin" || options.position === "beforeend";
1043 if (positionInside && this._childrenListNode) {
1044 this._htmlEditElement = document.createElement("li");
1046 let referenceNode = options.position === "afterbegin" ? this._childrenListNode.firstElementChild : this._childrenListNode.lastElementChild;
1047 this._childrenListNode.insertBefore(this._htmlEditElement, referenceNode);
1048 } else if (options.position && !positionInside) {
1049 this._htmlEditElement = document.createElement("li");
1051 let targetNode = (options.position === "afterend" && this._childrenListNode) ? this._childrenListNode : this.listItemElement;
1052 targetNode.insertAdjacentElement(options.position, this._htmlEditElement);
1054 this._htmlEditElement = document.createElement("div");
1055 this.listItemElement.appendChild(this._htmlEditElement);
1058 if (options.initialValue)
1059 this._htmlEditElement.textContent = options.initialValue;
1061 this.updateSelectionArea();
1065 commitCallback(this._htmlEditElement.textContent);
1071 this._editing = false;
1074 this._htmlEditElement.remove();
1075 this._htmlEditElement = null;
1077 if (options.hideExistingElements) {
1078 if (this._childrenListNode)
1079 this._childrenListNode.style.removeProperty("display");
1080 let child = this.listItemElement.firstChild;
1082 child.style.removeProperty("display");
1083 child = child.nextSibling;
1087 this.updateSelectionArea();
1090 var config = new WI.EditingConfig(commit.bind(this), dispose.bind(this));
1091 config.setMultiline(true);
1092 this._editing = WI.startEditing(this._htmlEditElement, config);
1094 if (options.initialValue && !isNaN(options.startPosition)) {
1095 let range = document.createRange();
1096 range.setStart(this._htmlEditElement.firstChild, options.startPosition);
1097 range.collapse(true);
1099 let selection = window.getSelection();
1100 selection.removeAllRanges();
1101 selection.addRange(range);
1105 _attributeEditingCommitted(element, newText, oldText, attributeName, moveDirection)
1107 this._editing = false;
1109 if (!newText.trim())
1112 if (!moveDirection && newText === oldText)
1115 // FIXME: Workaround for <https://webkit.org/b/123163> is forced on SPACE between text nodes.
1116 const nbspRegex = /\xA0/g;
1117 newText = newText.replace(nbspRegex, " ");
1119 var treeOutline = this.treeOutline;
1120 function moveToNextAttributeIfNeeded(error)
1123 this._editingCancelled(element, attributeName);
1128 treeOutline._updateModifiedNodes();
1130 // Search for the attribute's position, and then decide where to move to.
1131 var attributes = this.representedObject.attributes();
1132 for (var i = 0; i < attributes.length; ++i) {
1133 if (attributes[i].name !== attributeName)
1136 if (moveDirection === "backward") {
1138 this._startEditingTagName();
1140 this._triggerEditAttribute(attributes[i - 1].name);
1142 if (i === attributes.length - 1)
1143 this._addNewAttribute();
1145 this._triggerEditAttribute(attributes[i + 1].name);
1150 // Moving From the "New Attribute" position.
1151 if (moveDirection === "backward") {
1152 if (newText === " ") {
1153 // Moving from "New Attribute" that was not edited
1154 if (attributes.length)
1155 this._triggerEditAttribute(attributes.lastValue.name);
1157 // Moving from "New Attribute" that holds new value
1158 if (attributes.length > 1)
1159 this._triggerEditAttribute(attributes[attributes.length - 2].name);
1161 } else if (moveDirection === "forward") {
1162 if (!/^\s*$/.test(newText))
1163 this._addNewAttribute();
1165 this._startEditingTagName();
1169 this.representedObject.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
1172 _attributeNumberEditingCommitted(element, newText, oldText, attributeName, moveDirection)
1174 if (newText === oldText)
1177 this.representedObject.setAttribute(attributeName, newText);
1180 _tagNameEditingCommitted(element, newText, oldText, tagName, moveDirection)
1182 this._editing = false;
1187 var closingTagElement = self._distinctClosingTagElement();
1188 if (closingTagElement)
1189 closingTagElement.textContent = "</" + tagName + ">";
1191 self._editingCancelled(element, tagName);
1192 moveToNextAttributeIfNeeded.call(self);
1195 function moveToNextAttributeIfNeeded()
1197 if (moveDirection !== "forward") {
1198 this._addNewAttribute();
1202 var attributes = this.representedObject.attributes();
1203 if (attributes.length > 0)
1204 this._triggerEditAttribute(attributes[0].name);
1206 this._addNewAttribute();
1209 newText = newText.trim();
1210 if (newText === oldText) {
1215 var treeOutline = this.treeOutline;
1216 var wasExpanded = this.expanded;
1218 function changeTagNameCallback(error, nodeId)
1220 if (error || !nodeId) {
1225 var node = WI.domManager.nodeForId(nodeId);
1227 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1228 treeOutline._updateModifiedNodes();
1229 treeOutline.selectDOMNode(node, true);
1231 var newTreeItem = treeOutline.findTreeElement(node);
1233 newTreeItem.expand();
1235 moveToNextAttributeIfNeeded.call(newTreeItem);
1238 this.representedObject.setNodeName(newText, changeTagNameCallback);
1241 _textNodeEditingCommitted(element, newText)
1243 this._editing = false;
1246 if (this.representedObject.nodeType() === Node.ELEMENT_NODE) {
1247 // We only show text nodes inline in elements if the element only
1248 // has a single child, and that child is a text node.
1249 textNode = this.representedObject.firstChild;
1250 } else if (this.representedObject.nodeType() === Node.TEXT_NODE)
1251 textNode = this.representedObject;
1253 textNode.setNodeValue(newText, this.updateTitle.bind(this));
1256 _editingCancelled(element, context)
1258 this._editing = false;
1260 // Need to restore attributes structure.
1264 _distinctClosingTagElement()
1266 // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
1268 // For an expanded element, it will be the last element with class "close"
1269 // in the child element list.
1270 if (this.expanded) {
1271 var closers = this._childrenListNode.querySelectorAll(".close");
1272 return closers[closers.length - 1];
1275 // Remaining cases are single line non-expanded elements with a closing
1276 // tag, or HTML elements without a closing tag (such as <br>). Return
1277 // null in the case where there isn't a closing tag.
1278 var tags = this.listItemElement.getElementsByClassName("html-tag");
1279 return tags.length === 1 ? null : tags[tags.length - 1];
1282 updateTitle(onlySearchQueryChanged)
1284 // If we are editing, return early to prevent canceling the edit.
1285 // After editing is committed updateTitle will be called.
1286 if (this._editing && !this._forceUpdateTitle)
1289 if (onlySearchQueryChanged) {
1290 if (this._highlightResult)
1291 this._updateSearchHighlight(false);
1293 this.title = document.createElement("span");
1294 this.title.appendChild(this._nodeTitleInfo().titleDOM);
1295 this._highlightResult = undefined;
1298 // Setting this.title will implicitly remove all children. Clear the
1299 // selection element so that we properly recreate it if necessary.
1300 this._selectionElement = null;
1301 this.updateSelectionArea();
1302 this._highlightSearchResults();
1303 this._updatePseudoClassIndicator();
1304 this._updateBreakpointStatus();
1307 _buildAttributeDOM(parentElement, name, value, node)
1309 let hasText = value.length > 0;
1310 let attrSpanElement = parentElement.createChild("span", "html-attribute");
1311 let attrNameElement = attrSpanElement.createChild("span", "html-attribute-name");
1312 attrNameElement.textContent = name;
1313 let attrValueElement = null;
1315 attrSpanElement.append("=\u200B\"");
1317 if (name === "src" || /\bhref\b/.test(name)) {
1318 let baseURL = node.frame ? node.frame.url : null;
1319 let rewrittenURL = absoluteURL(value, baseURL);
1320 value = value.insertWordBreakCharacters();
1321 if (!rewrittenURL) {
1322 attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1323 attrValueElement.textContent = value;
1325 if (value.startsWith("data:"))
1326 value = value.truncateMiddle(60);
1328 attrValueElement = document.createElement("a");
1329 attrValueElement.href = rewrittenURL;
1330 attrValueElement.textContent = value;
1331 attrSpanElement.appendChild(attrValueElement);
1333 } else if (name === "srcset") {
1334 let baseURL = node.frame ? node.frame.url : null;
1335 attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1337 // Leading whitespace.
1338 let groups = value.split(/\s*,\s*/);
1339 for (let i = 0; i < groups.length; ++i) {
1340 let string = groups[i].trim();
1341 let spaceIndex = string.search(/\s/);
1343 if (spaceIndex === -1) {
1344 let linkText = string;
1345 let rewrittenURL = absoluteURL(string, baseURL);
1346 let linkElement = attrValueElement.appendChild(document.createElement("a"));
1347 linkElement.href = rewrittenURL;
1348 linkElement.textContent = linkText.insertWordBreakCharacters();
1350 let linkText = string.substring(0, spaceIndex);
1351 let descriptorText = string.substring(spaceIndex).insertWordBreakCharacters();
1352 let rewrittenURL = absoluteURL(linkText, baseURL);
1353 let linkElement = attrValueElement.appendChild(document.createElement("a"));
1354 linkElement.href = rewrittenURL;
1355 linkElement.textContent = linkText.insertWordBreakCharacters();
1356 let descriptorElement = attrValueElement.appendChild(document.createElement("span"));
1357 descriptorElement.textContent = descriptorText;
1360 if (i < groups.length - 1) {
1361 let commaElement = attrValueElement.appendChild(document.createElement("span"));
1362 commaElement.textContent = ", ";
1366 value = value.insertWordBreakCharacters();
1367 attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1368 attrValueElement.textContent = value;
1372 attrSpanElement.append("\"");
1374 this._createModifiedAnimation(name, value, hasText ? attrValueElement : attrNameElement);
1376 if (this._highlightedAttributes.has(name))
1377 attrSpanElement.classList.add("highlight");
1380 _buildTagDOM({parentElement, tagName, isClosingTag, isDistinctTreeElement, willRenderCloseTagInline})
1382 var node = this.representedObject;
1383 var classes = ["html-tag"];
1384 if (isClosingTag && isDistinctTreeElement)
1385 classes.push("close");
1386 var tagElement = parentElement.createChild("span", classes.join(" "));
1387 tagElement.append("<");
1388 var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "html-tag-name");
1389 tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
1390 if (!isClosingTag && node.hasAttributes()) {
1391 var attributes = node.attributes();
1392 for (var i = 0; i < attributes.length; ++i) {
1393 var attr = attributes[i];
1394 tagElement.append(" ");
1395 this._buildAttributeDOM(tagElement, attr.name, attr.value, node);
1398 tagElement.append(">");
1399 parentElement.append("\u200B");
1401 if (this._showGoToArrow && node.nodeType() === Node.ELEMENT_NODE && willRenderCloseTagInline === isClosingTag) {
1402 let goToArrowElement = parentElement.appendChild(WI.createGoToArrowButton());
1403 goToArrowElement.title = WI.UIString("Reveal in Elements Tab");
1404 goToArrowElement.addEventListener("click", (event) => {
1405 WI.domManager.inspectElement(this.representedObject.id);
1412 var node = this.representedObject;
1413 var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
1415 function trimedNodeValue()
1417 // Trim empty lines from the beginning and extra space at the end since most style and script tags begin with a newline
1418 // and end with a newline and indentation for the end tag.
1419 return node.nodeValue().replace(/^[\n\r]*/, "").replace(/\s*$/, "");
1422 switch (node.nodeType()) {
1423 case Node.DOCUMENT_FRAGMENT_NODE:
1424 var fragmentElement = info.titleDOM.createChild("span", "html-fragment");
1425 if (node.shadowRootType()) {
1426 fragmentElement.textContent = WI.UIString("Shadow Content (%s)").format(WI.DOMTreeElement.shadowRootTypeDisplayName(node.shadowRootType()));
1427 this.listItemElement.classList.add("shadow");
1428 } else if (node.parentNode && node.parentNode.templateContent() === node) {
1429 fragmentElement.textContent = WI.UIString("Template Content");
1430 this.listItemElement.classList.add("template");
1432 fragmentElement.textContent = WI.UIString("Document Fragment");
1433 this.listItemElement.classList.add("fragment");
1437 case Node.ATTRIBUTE_NODE:
1438 var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
1439 this._buildAttributeDOM(info.titleDOM, node.name, value);
1442 case Node.ELEMENT_NODE:
1443 if (node.isPseudoElement()) {
1444 var pseudoElement = info.titleDOM.createChild("span", "html-pseudo-element");
1445 pseudoElement.textContent = "::" + node.pseudoType();
1446 info.titleDOM.appendChild(document.createTextNode("\u200B"));
1447 info.hasChildren = false;
1451 var tagName = node.nodeNameInCorrectCase();
1452 if (this._elementCloseTag) {
1454 parentElement: info.titleDOM,
1457 isDistinctTreeElement: true,
1458 willRenderCloseTagInline: false,
1460 info.hasChildren = false;
1464 var textChild = this._singleTextChild(node);
1465 var showInlineText = textChild && textChild.nodeValue().length < WI.DOMTreeElement.MaximumInlineTextChildLength;
1466 var showInlineEllipsis = !this.expanded && !showInlineText && (this.treeOutline.isXMLMimeType || !WI.DOMTreeElement.ForbiddenClosingTagElements.has(tagName));
1469 parentElement: info.titleDOM,
1471 isClosingTag: false,
1472 isDistinctTreeElement: false,
1473 willRenderCloseTagInline: showInlineText || showInlineEllipsis,
1476 if (showInlineEllipsis) {
1477 if (this.hasChildren) {
1478 var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1479 textNodeElement.textContent = ellipsis;
1480 info.titleDOM.append("\u200B");
1483 parentElement: info.titleDOM,
1486 isDistinctTreeElement: false,
1487 willRenderCloseTagInline: true,
1491 // If this element only has a single child that is a text node,
1492 // just show that text and the closing tag inline rather than
1493 // create a subtree for them
1494 if (showInlineText) {
1495 var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1496 var nodeNameLowerCase = node.nodeName().toLowerCase();
1498 if (nodeNameLowerCase === "script")
1499 textNodeElement.appendChild(WI.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/javascript"));
1500 else if (nodeNameLowerCase === "style")
1501 textNodeElement.appendChild(WI.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/css"));
1503 textNodeElement.textContent = textChild.nodeValue();
1505 info.titleDOM.append("\u200B");
1508 parentElement: info.titleDOM,
1511 isDistinctTreeElement: false,
1512 willRenderCloseTagInline: true,
1514 info.hasChildren = false;
1518 case Node.TEXT_NODE:
1519 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
1520 var newNode = info.titleDOM.createChild("span", "html-text-node large");
1521 newNode.appendChild(WI.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/javascript"));
1522 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
1523 var newNode = info.titleDOM.createChild("span", "html-text-node large");
1524 newNode.appendChild(WI.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/css"));
1526 info.titleDOM.append("\"");
1527 var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1528 textNodeElement.textContent = node.nodeValue();
1529 info.titleDOM.append("\"");
1533 case Node.COMMENT_NODE:
1534 var commentElement = info.titleDOM.createChild("span", "html-comment");
1535 commentElement.append("<!--" + node.nodeValue() + "-->");
1538 case Node.DOCUMENT_TYPE_NODE:
1539 var docTypeElement = info.titleDOM.createChild("span", "html-doctype");
1540 docTypeElement.append("<!DOCTYPE " + node.nodeName());
1541 if (node.publicId) {
1542 docTypeElement.append(" PUBLIC \"" + node.publicId + "\"");
1544 docTypeElement.append(" \"" + node.systemId + "\"");
1545 } else if (node.systemId)
1546 docTypeElement.append(" SYSTEM \"" + node.systemId + "\"");
1548 docTypeElement.append(">");
1551 case Node.CDATA_SECTION_NODE:
1552 var cdataElement = info.titleDOM.createChild("span", "html-text-node");
1553 cdataElement.append("<![CDATA[" + node.nodeValue() + "]]>");
1556 case Node.PROCESSING_INSTRUCTION_NODE:
1557 var processingInstructionElement = info.titleDOM.createChild("span", "html-processing-instruction");
1558 var data = node.nodeValue();
1559 var dataString = data.length ? " " + data : "";
1560 var title = "<?" + node.nodeNameInCorrectCase() + dataString + "?>";
1561 processingInstructionElement.append(title);
1565 info.titleDOM.append(node.nodeNameInCorrectCase().collapseWhitespace());
1571 _singleTextChild(node)
1573 if (!node || this._ignoreSingleTextChild)
1576 var firstChild = node.firstChild;
1577 if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE)
1580 if (node.hasShadowRoots())
1582 if (node.templateContent())
1584 if (node.hasPseudoElements())
1587 var sibling = firstChild.nextSibling;
1588 return sibling ? null : firstChild;
1591 _showInlineText(node)
1593 if (node.nodeType() === Node.ELEMENT_NODE) {
1594 var textChild = this._singleTextChild(node);
1595 if (textChild && textChild.nodeValue().length < WI.DOMTreeElement.MaximumInlineTextChildLength)
1601 _hasVisibleChildren()
1603 var node = this.representedObject;
1605 if (this._showInlineText(node))
1608 if (node.hasChildNodes())
1610 if (node.templateContent())
1612 if (node.hasPseudoElements())
1620 var node = this.representedObject;
1622 var visibleChildren = [];
1624 var templateContent = node.templateContent();
1625 if (templateContent)
1626 visibleChildren.push(templateContent);
1628 var beforePseudoElement = node.beforePseudoElement();
1629 if (beforePseudoElement)
1630 visibleChildren.push(beforePseudoElement);
1632 if (node.childNodeCount && node.children)
1633 visibleChildren.pushAll(node.children);
1635 var afterPseudoElement = node.afterPseudoElement();
1636 if (afterPseudoElement)
1637 visibleChildren.push(afterPseudoElement);
1639 return visibleChildren;
1644 var parentElement = this.parent;
1649 function removeNodeCallback(error, removedNodeId)
1657 parentElement.removeChild(self);
1658 parentElement.adjustCollapsedRange();
1661 this.representedObject.removeNode(removeNodeCallback);
1664 _insertAdjacentHTML(position, options = {})
1666 let hasChildren = this.hasChildren;
1668 let commitCallback = (value) => {
1669 this._ignoreSingleTextChild = false;
1671 if (!value.length) {
1673 this._forceUpdateTitle = true;
1674 this.hasChildren = false;
1675 this._forceUpdateTitle = false;
1680 this.representedObject.insertAdjacentHTML(position, value);
1683 if (position === "afterbegin" || position === "beforeend") {
1684 this._ignoreSingleTextChild = true;
1685 this.hasChildren = true;
1689 this._startEditingAsHTML(commitCallback, {...options, position});
1695 switch (this.representedObject.nodeNameInCorrectCase()) {
1698 options.initialValue = "<li></li>";
1699 options.startPosition = 4;
1705 options.initialValue = "<tr></tr>";
1706 options.startPosition = 4;
1709 options.initializing = "<td></td>";
1710 options.startPosition = 4;
1713 this._insertAdjacentHTML("beforeend", options);
1716 _addPreviousSibling(event)
1719 let nodeName = this.representedObject.nodeNameInCorrectCase();
1720 if (nodeName === "li" || nodeName === "tr" || nodeName === "th" || nodeName === "td") {
1721 options.initialValue = `<${nodeName}></${nodeName}>`;
1722 options.startPosition = nodeName.length + 2;
1724 this._insertAdjacentHTML("beforebegin", options);
1727 _addNextSibling(event)
1730 let nodeName = this.representedObject.nodeNameInCorrectCase();
1731 if (nodeName === "li" || nodeName === "tr" || nodeName === "th" || nodeName === "td") {
1732 options.initialValue = `<${nodeName}></${nodeName}>`;
1733 options.startPosition = nodeName.length + 2;
1735 this._insertAdjacentHTML("afterend", options);
1740 var treeOutline = this.treeOutline;
1741 var node = this.representedObject;
1742 var parentNode = node.parentNode;
1743 var index = node.index;
1744 var wasExpanded = this.expanded;
1746 function selectNode(error, nodeId)
1751 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1752 treeOutline._updateModifiedNodes();
1754 var newNode = parentNode ? parentNode.children[index] || parentNode : null;
1758 treeOutline.selectDOMNode(newNode, true);
1761 var newTreeItem = treeOutline.findTreeElement(newNode);
1763 newTreeItem.expand();
1767 function commitChange(value)
1769 node.setOuterHTML(value, selectNode);
1772 node.getOuterHTML((error, initialValue) => {
1776 this._startEditingAsHTML(commitChange, {
1778 hideExistingElements: true,
1785 this.representedObject.copyNode();
1788 _highlightSearchResults()
1790 if (!this.title || !this._searchQuery || !this._searchHighlightsVisible)
1793 if (this._highlightResult) {
1794 this._updateSearchHighlight(true);
1798 var text = this.title.textContent;
1799 let searchRegex = WI.SearchUtilities.regExpForString(this._searchQuery, WI.SearchUtilities.defaultSettings);
1801 var match = searchRegex.exec(text);
1802 var matchRanges = [];
1804 matchRanges.push({offset: match.index, length: match[0].length});
1805 match = searchRegex.exec(text);
1808 // Fall back for XPath, etc. matches.
1809 if (!matchRanges.length)
1810 matchRanges.push({offset: 0, length: text.length});
1812 this._highlightResult = [];
1813 WI.highlightRangesWithStyleClass(this.title, matchRanges, WI.DOMTreeElement.SearchHighlightStyleClassName, this._highlightResult);
1816 _createModifiedAnimation(key, value, element)
1818 let existing = this._recentlyModifiedAttributes.get(key);
1822 if (existing.element) {
1823 if (existing.listener)
1824 existing.element.removeEventListener("animationend", existing.listener);
1826 existing.element.classList.remove("node-state-changed");
1827 existing.element.style.removeProperty("animation-delay");
1830 existing.listener = (event) => {
1831 element.classList.remove("node-state-changed");
1832 element.style.removeProperty("animation-delay");
1834 this._recentlyModifiedAttributes.delete(key);
1837 element.classList.remove("node-state-changed");
1838 element.style.removeProperty("animation-delay");
1840 if (existing.value === value)
1841 element.style.setProperty("animation-delay", "-" + (performance.now() - existing.timestamp) + "ms");
1843 existing.timestamp = performance.now();
1845 existing.value = value;
1846 existing.element = element;
1848 element.addEventListener("animationend", existing.listener, {once: true});
1849 element.classList.add("node-state-changed");
1854 let classes = this.representedObject.getAttribute("class");
1855 return classes && classes.includes(WI.DOMTreeElement.HideElementStyleSheetIdOrClassName);
1858 _updatePseudoClassIndicator()
1860 if (!this.listItemElement || this._elementCloseTag)
1863 if (this.representedObject.enabledPseudoClasses.length) {
1864 if (!this._pseudoClassIndicatorElement) {
1865 this._pseudoClassIndicatorElement = document.createElement("div");
1866 this._pseudoClassIndicatorElement.classList.add("pseudo-class-indicator");
1868 this.listItemElement.insertBefore(this._pseudoClassIndicatorElement, this.listItemElement.firstChild);
1870 if (this._pseudoClassIndicatorElement) {
1871 this._pseudoClassIndicatorElement.remove();
1872 this._pseudoClassIndicatorElement = null;
1879 if (event.type === "dragstart" && this._editing)
1880 event.preventDefault();
1883 _subtreeBreakpointChanged(treeElement)
1885 if (treeElement.hasBreakpoint) {
1886 if (!this._subtreeBreakpointTreeElements)
1887 this._subtreeBreakpointTreeElements = new Set;
1888 this._subtreeBreakpointTreeElements.add(treeElement);
1890 this._subtreeBreakpointTreeElements.delete(treeElement);
1891 if (!this._subtreeBreakpointTreeElements.size)
1892 this._subtreeBreakpointTreeElements = null;
1895 this._updateBreakpointStatus();
1898 _updateBreakpointStatus()
1900 let listItemElement = this.listItemElement;
1901 if (!listItemElement)
1904 let hasBreakpoint = this._breakpointStatus !== WI.DOMTreeElement.BreakpointStatus.None;
1905 let hasSubtreeBreakpoints = this._subtreeBreakpointTreeElements && this._subtreeBreakpointTreeElements.size;
1907 if (!hasBreakpoint && !hasSubtreeBreakpoints) {
1908 if (this._statusImageElement)
1909 this._statusImageElement.remove();
1913 if (!this._statusImageElement) {
1914 this._statusImageElement = WI.ImageUtilities.useSVGSymbol("Images/DOMBreakpoint.svg", "status-image");
1915 this._statusImageElement.classList.add("breakpoint");
1916 this._statusImageElement.addEventListener("click", this._statusImageClicked.bind(this));
1917 this._statusImageElement.addEventListener("contextmenu", this._statusImageContextmenu.bind(this));
1918 this._statusImageElement.addEventListener("mousedown", (event) => { event.stopPropagation(); });
1921 this._statusImageElement.classList.toggle("subtree", !hasBreakpoint && hasSubtreeBreakpoints);
1923 this.listItemElement.insertBefore(this._statusImageElement, this.listItemElement.firstChild);
1925 let disabled = this._breakpointStatus === WI.DOMTreeElement.BreakpointStatus.DisabledBreakpoint;
1926 this._statusImageElement.classList.toggle("disabled", disabled);
1929 _statusImageClicked(event)
1931 if (this._breakpointStatus === WI.DOMTreeElement.BreakpointStatus.None)
1934 if (event.button !== 0 || event.ctrlKey)
1937 let breakpoints = WI.domDebuggerManager.domBreakpointsForNode(this.representedObject);
1938 if (!breakpoints || !breakpoints.length)
1941 let shouldEnable = breakpoints.some((breakpoint) => breakpoint.disabled);
1942 breakpoints.forEach((breakpoint) => { breakpoint.disabled = !shouldEnable });
1945 _statusImageContextmenu(event)
1947 if (!this.hasBreakpoint)
1950 let contextMenu = WI.ContextMenu.createFromEvent(event);
1952 WI.appendContextMenuItemsForDOMNodeBreakpoints(contextMenu, this.representedObject, {
1953 revealDescendantBreakpointsMenuItemHandler: this.bindRevealDescendantBreakpointsMenuItemHandler(),
1957 _highlightAnimationEnd()
1959 let listItemElement = this.listItemElement;
1960 if (!listItemElement)
1963 listItemElement.removeEventListener("animationend", this._boundHighlightAnimationEnd);
1964 listItemElement.classList.remove(WI.DOMTreeElement.HighlightStyleClassName);
1966 this._animatingHighlight = false;
1970 WI.DOMTreeElement.InitialChildrenLimit = 500;
1971 WI.DOMTreeElement.MaximumInlineTextChildLength = 80;
1973 // A union of HTML4 and HTML5-Draft elements that explicitly
1974 // or implicitly (for HTML5) forbid the closing tag.
1975 WI.DOMTreeElement.ForbiddenClosingTagElements = new Set([
1976 "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
1977 "hr", "img", "input", "keygen", "link", "meta", "param", "source",
1978 "wbr", "track", "menuitem"
1981 // These tags we do not allow editing their tag name.
1982 WI.DOMTreeElement.EditTagBlacklist = new Set([
1983 "html", "head", "body"
1986 WI.DOMTreeElement.BreakpointStatus = {
1987 None: Symbol("none"),
1988 Breakpoint: Symbol("breakpoint"),
1989 DisabledBreakpoint: Symbol("disabled-breakpoint"),
1992 WI.DOMTreeElement.HighlightStyleClassName = "highlight";
1993 WI.DOMTreeElement.SearchHighlightStyleClassName = "search-highlight";
1994 WI.DOMTreeElement.BouncyHighlightStyleClassName = "bouncy-highlight";
1995 WI.DOMTreeElement.HideElementStyleSheetIdOrClassName = "__WebInspectorHideElement__";