debug-server-next
Version:
Dev server for hippy-core.
1,217 lines • 55.2 kB
JavaScript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/*
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
* Copyright (C) 2009 Joseph Pecoraro
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* eslint-disable rulesdir/no_underscored_properties */
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as UI from '../../ui/legacy/legacy.js';
import { linkifyDeferredNodeReference } from './DOMLinkifier.js';
import { ElementsPanel } from './ElementsPanel.js';
import { ElementsTreeElement, InitialChildrenLimit } from './ElementsTreeElement.js';
import { ImagePreviewPopover } from './ImagePreviewPopover.js';
const UIStrings = {
/**
*@description ARIA accessible name in Elements Tree Outline of the Elements panel
*/
pageDom: 'Page DOM',
/**
*@description A context menu item to store a value as a global variable the Elements Panel
*/
storeAsGlobalVariable: 'Store as global variable',
/**
*@description Tree element expand all button element button text content in Elements Tree Outline of the Elements panel
*@example {3} PH1
*/
showAllNodesDMore: 'Show All Nodes ({PH1} More)',
/**
*@description Link text content in Elements Tree Outline of the Elements panel
*/
reveal: 'reveal',
/**
* @description A context menu item to open the badge settings pane
*/
adornerSettings: 'Badge settings\u2026',
};
const str_ = i18n.i18n.registerUIStrings('panels/elements/ElementsTreeOutline.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const elementsTreeOutlineByDOMModel = new WeakMap();
const populatedTreeElements = new Set();
export class ElementsTreeOutline extends UI.TreeOutline.TreeOutline {
treeElementByNode;
_shadowRoot;
_element;
_includeRootDOMNode;
_selectEnabled;
_rootDOMNode;
_selectedDOMNode;
_visible;
_imagePreviewPopover;
_updateRecords;
_treeElementsBeingUpdated;
decoratorExtensions;
_showHTMLCommentsSetting;
_multilineEditing;
_visibleWidth;
_clipboardNodeData;
_isXMLMimeType;
suppressRevealAndSelect = false;
_previousHoveredElement;
_treeElementBeingDragged;
_dragOverTreeElement;
_updateModifiedNodesTimeout;
constructor(omitRootDOMNode, selectEnabled, hideGutter) {
super();
this.treeElementByNode = new WeakMap();
const shadowContainer = document.createElement('div');
this._shadowRoot = UI.Utils.createShadowRootWithCoreStyles(shadowContainer, { cssFile: 'panels/elements/elementsTreeOutline.css', delegatesFocus: undefined });
const outlineDisclosureElement = this._shadowRoot.createChild('div', 'elements-disclosure');
this._element = this.element;
this._element.classList.add('elements-tree-outline', 'source-code');
if (hideGutter) {
this._element.classList.add('elements-hide-gutter');
}
UI.ARIAUtils.setAccessibleName(this._element, i18nString(UIStrings.pageDom));
this._element.addEventListener('focusout', this._onfocusout.bind(this), false);
this._element.addEventListener('mousedown', this._onmousedown.bind(this), false);
this._element.addEventListener('mousemove', this._onmousemove.bind(this), false);
this._element.addEventListener('mouseleave', this._onmouseleave.bind(this), false);
this._element.addEventListener('dragstart', this._ondragstart.bind(this), false);
this._element.addEventListener('dragover', this._ondragover.bind(this), false);
this._element.addEventListener('dragleave', this._ondragleave.bind(this), false);
this._element.addEventListener('drop', this._ondrop.bind(this), false);
this._element.addEventListener('dragend', this._ondragend.bind(this), false);
this._element.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false);
this._element.addEventListener('clipboard-beforecopy', this._onBeforeCopy.bind(this), false);
this._element.addEventListener('clipboard-copy', this._onCopyOrCut.bind(this, false), false);
this._element.addEventListener('clipboard-cut', this._onCopyOrCut.bind(this, true), false);
this._element.addEventListener('clipboard-paste', this._onPaste.bind(this), false);
this._element.addEventListener('keydown', this._onKeyDown.bind(this), false);
outlineDisclosureElement.appendChild(this._element);
this.element = shadowContainer;
this._includeRootDOMNode = !omitRootDOMNode;
this._selectEnabled = selectEnabled;
this._rootDOMNode = null;
this._selectedDOMNode = null;
this._visible = false;
this._imagePreviewPopover = new ImagePreviewPopover(this.contentElement, event => {
let link = event.target;
while (link && !ImagePreviewPopover.getImageURL(link)) {
link = link.parentElementOrShadowHost();
}
return link;
}, link => {
const listItem = UI.UIUtils.enclosingNodeOrSelfWithNodeName(link, 'li');
if (!listItem) {
return null;
}
const treeElement = UI.TreeOutline.TreeElement.getTreeElementBylistItemNode(listItem);
if (!treeElement) {
return null;
}
return treeElement.node();
});
this._updateRecords = new Map();
this._treeElementsBeingUpdated = new Set();
this.decoratorExtensions = null;
this._showHTMLCommentsSetting = Common.Settings.Settings.instance().moduleSetting('showHTMLComments');
this._showHTMLCommentsSetting.addChangeListener(this._onShowHTMLCommentsChange.bind(this));
this.useLightSelectionColor();
}
static forDOMModel(domModel) {
return elementsTreeOutlineByDOMModel.get(domModel) || null;
}
_onShowHTMLCommentsChange() {
const selectedNode = this.selectedDOMNode();
if (selectedNode && selectedNode.nodeType() === Node.COMMENT_NODE && !this._showHTMLCommentsSetting.get()) {
this.selectDOMNode(selectedNode.parentNode);
}
this.update();
}
setWordWrap(wrap) {
this._element.classList.toggle('elements-tree-nowrap', !wrap);
}
setMultilineEditing(multilineEditing) {
this._multilineEditing = multilineEditing;
}
visibleWidth() {
return this._visibleWidth || 0;
}
setVisibleWidth(width) {
this._visibleWidth = width;
if (this._multilineEditing) {
this._multilineEditing.resize();
}
}
_setClipboardData(data) {
if (this._clipboardNodeData) {
const treeElement = this.findTreeElement(this._clipboardNodeData.node);
if (treeElement) {
treeElement.setInClipboard(false);
}
delete this._clipboardNodeData;
}
if (data) {
const treeElement = this.findTreeElement(data.node);
if (treeElement) {
treeElement.setInClipboard(true);
}
this._clipboardNodeData = data;
}
}
resetClipboardIfNeeded(removedNode) {
if (this._clipboardNodeData && this._clipboardNodeData.node === removedNode) {
this._setClipboardData(null);
}
}
_onBeforeCopy(event) {
event.handled = true;
}
_onCopyOrCut(isCut, event) {
this._setClipboardData(null);
// @ts-ignore this bound in the main entry point
const originalEvent = event['original'];
if (!originalEvent || !originalEvent.target) {
return;
}
// Don't prevent the normal copy if the user has a selection.
if (originalEvent.target instanceof Node && originalEvent.target.hasSelection()) {
return;
}
// Do not interfere with text editing.
if (UI.UIUtils.isEditing()) {
return;
}
const targetNode = this.selectedDOMNode();
if (!targetNode) {
return;
}
if (!originalEvent.clipboardData) {
return;
}
originalEvent.clipboardData.clearData();
event.handled = true;
this.performCopyOrCut(isCut, targetNode);
}
performCopyOrCut(isCut, node) {
if (!node) {
return;
}
if (isCut && (node.isShadowRoot() || node.ancestorUserAgentShadowRoot())) {
return;
}
node.copyNode();
this._setClipboardData({ node: node, isCut: isCut });
}
canPaste(targetNode) {
if (targetNode.isShadowRoot() || targetNode.ancestorUserAgentShadowRoot()) {
return false;
}
if (!this._clipboardNodeData) {
return false;
}
const node = this._clipboardNodeData.node;
if (this._clipboardNodeData.isCut && (node === targetNode || node.isAncestor(targetNode))) {
return false;
}
if (targetNode.domModel() !== node.domModel()) {
return false;
}
return true;
}
pasteNode(targetNode) {
if (this.canPaste(targetNode)) {
this._performPaste(targetNode);
}
}
duplicateNode(targetNode) {
this._performDuplicate(targetNode);
}
_onPaste(event) {
// Do not interfere with text editing.
if (UI.UIUtils.isEditing()) {
return;
}
const targetNode = this.selectedDOMNode();
if (!targetNode || !this.canPaste(targetNode)) {
return;
}
event.handled = true;
this._performPaste(targetNode);
}
_performPaste(targetNode) {
if (!this._clipboardNodeData) {
return;
}
if (this._clipboardNodeData.isCut) {
this._clipboardNodeData.node.moveTo(targetNode, null, expandCallback.bind(this));
this._setClipboardData(null);
}
else {
this._clipboardNodeData.node.copyTo(targetNode, null, expandCallback.bind(this));
}
function expandCallback(error, pastedNode) {
if (error || !pastedNode) {
return;
}
this.selectDOMNode(pastedNode);
}
}
_performDuplicate(targetNode) {
if (targetNode.isInShadowTree()) {
return;
}
const parentNode = targetNode.parentNode ? targetNode.parentNode : targetNode;
if (parentNode.nodeName() === '#document') {
return;
}
targetNode.copyTo(parentNode, targetNode.nextSibling);
}
setVisible(visible) {
if (visible === this._visible) {
return;
}
this._visible = visible;
if (!this._visible) {
this._imagePreviewPopover.hide();
if (this._multilineEditing) {
this._multilineEditing.cancel();
}
return;
}
this.runPendingUpdates();
if (this._selectedDOMNode) {
this._revealAndSelectNode(this._selectedDOMNode, false);
}
}
get rootDOMNode() {
return this._rootDOMNode;
}
set rootDOMNode(x) {
if (this._rootDOMNode === x) {
return;
}
this._rootDOMNode = x;
this._isXMLMimeType = x && x.isXMLNode();
this.update();
}
get isXMLMimeType() {
return Boolean(this._isXMLMimeType);
}
selectedDOMNode() {
return this._selectedDOMNode;
}
selectDOMNode(node, focus) {
if (this._selectedDOMNode === node) {
this._revealAndSelectNode(node, !focus);
return;
}
this._selectedDOMNode = node;
this._revealAndSelectNode(node, !focus);
// The _revealAndSelectNode() method might find a different element if there is inlined text,
// and the select() call would change the selectedDOMNode and reenter this setter. So to
// avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
// node as the one passed in.
if (this._selectedDOMNode === node) {
this._selectedNodeChanged(Boolean(focus));
}
}
editing() {
const node = this.selectedDOMNode();
if (!node) {
return false;
}
const treeElement = this.findTreeElement(node);
if (!treeElement) {
return false;
}
return treeElement.isEditing() || false;
}
update() {
const selectedNode = this.selectedDOMNode();
this.removeChildren();
if (!this.rootDOMNode) {
return;
}
if (this._includeRootDOMNode) {
const treeElement = this._createElementTreeElement(this.rootDOMNode);
this.appendChild(treeElement);
}
else {
// FIXME: this could use findTreeElement to reuse a tree element if it already exists
const children = this._visibleChildren(this.rootDOMNode);
for (const child of children) {
const treeElement = this._createElementTreeElement(child);
this.appendChild(treeElement);
}
}
if (selectedNode) {
this._revealAndSelectNode(selectedNode, true);
}
}
_selectedNodeChanged(focus) {
this.dispatchEventToListeners(ElementsTreeOutline.Events.SelectedNodeChanged, { node: this._selectedDOMNode, focus: focus });
}
_fireElementsTreeUpdated(nodes) {
this.dispatchEventToListeners(ElementsTreeOutline.Events.ElementsTreeUpdated, nodes);
}
findTreeElement(node) {
let treeElement = this._lookUpTreeElement(node);
if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
// The text node might have been inlined if it was short, so try to find the parent element.
treeElement = this._lookUpTreeElement(node.parentNode);
}
return /** @type {?ElementsTreeElement} */ treeElement;
}
_lookUpTreeElement(node) {
if (!node) {
return null;
}
const cachedElement = this.treeElementByNode.get(node);
if (cachedElement) {
return cachedElement;
}
// Walk up the parent pointers from the desired node
const ancestors = [];
let currentNode;
for (currentNode = node.parentNode; currentNode; currentNode = currentNode.parentNode) {
ancestors.push(currentNode);
if (this.treeElementByNode.has(currentNode)) { // stop climbing as soon as we hit
break;
}
}
if (!currentNode) {
return null;
}
// Walk down to populate each ancestor's children, to fill in the tree and the cache.
for (let i = ancestors.length - 1; i >= 0; --i) {
const child = ancestors[i - 1] || node;
const treeElement = this.treeElementByNode.get(ancestors[i]);
if (treeElement) {
treeElement.onpopulate(); // fill the cache with the children of treeElement
if (child.index && child.index >= treeElement.expandedChildrenLimit()) {
this.setExpandedChildrenLimit(treeElement, child.index + 1);
}
}
}
return this.treeElementByNode.get(node) || null;
}
createTreeElementFor(node) {
let treeElement = this.findTreeElement(node);
if (treeElement) {
return treeElement;
}
if (!node.parentNode) {
return null;
}
treeElement = this.createTreeElementFor(node.parentNode);
return treeElement ? this._showChild(treeElement, node) : null;
}
_revealAndSelectNode(node, omitFocus) {
if (this.suppressRevealAndSelect) {
return;
}
if (!this._includeRootDOMNode && node === this.rootDOMNode && this.rootDOMNode) {
node = this.rootDOMNode.firstChild;
}
if (!node) {
return;
}
const treeElement = this.createTreeElementFor(node);
if (!treeElement) {
return;
}
treeElement.revealAndSelect(omitFocus);
}
_treeElementFromEvent(event) {
const scrollContainer = this.element.parentElement;
if (!scrollContainer) {
return null;
}
// We choose this X coordinate based on the knowledge that our list
// items extend at least to the right edge of the outer <ol> container.
// In the no-word-wrap mode the outer <ol> may be wider than the tree container
// (and partially hidden), in which case we are left to use only its right boundary.
const x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36;
const y = event.pageY;
// Our list items have 1-pixel cracks between them vertically. We avoid
// the cracks by checking slightly above and slightly below the mouse
// and seeing if we hit the same element each time.
const elementUnderMouse = this.treeElementFromPoint(x, y);
const elementAboveMouse = this.treeElementFromPoint(x, y - 2);
let element;
if (elementUnderMouse === elementAboveMouse) {
element = elementUnderMouse;
}
else {
element = this.treeElementFromPoint(x, y + 2);
}
return element;
}
_onfocusout(_event) {
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
}
_onmousedown(event) {
const element = this._treeElementFromEvent(event);
if (!element || element.isEventWithinDisclosureTriangle(event)) {
return;
}
element.select();
}
setHoverEffect(treeElement) {
if (this._previousHoveredElement === treeElement) {
return;
}
if (this._previousHoveredElement instanceof ElementsTreeElement) {
this._previousHoveredElement.hovered = false;
delete this._previousHoveredElement;
}
if (treeElement instanceof ElementsTreeElement) {
treeElement.hovered = true;
this._previousHoveredElement = treeElement;
}
}
_onmousemove(event) {
const element = this._treeElementFromEvent(event);
if (element && this._previousHoveredElement === element) {
return;
}
this.setHoverEffect(element);
this._highlightTreeElement(element, !UI.KeyboardShortcut.KeyboardShortcut.eventHasEitherCtrlOrMeta(event));
}
_highlightTreeElement(element, showInfo) {
if (element instanceof ElementsTreeElement) {
element.node().domModel().overlayModel().highlightInOverlay({ node: element.node(), selectorList: undefined }, 'all', showInfo);
return;
}
if (element instanceof ShortcutTreeElement) {
element.domModel().overlayModel().highlightInOverlay({ deferredNode: element.deferredNode(), selectorList: undefined }, 'all', showInfo);
}
}
_onmouseleave(_event) {
this.setHoverEffect(null);
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
}
_ondragstart(event) {
const node = event.target;
if (!node || node.hasSelection()) {
return false;
}
if (node.nodeName === 'A') {
return false;
}
const treeElement = this._validDragSourceOrTarget(this._treeElementFromEvent(event));
if (!treeElement) {
return false;
}
if (treeElement.node().nodeName() === 'BODY' || treeElement.node().nodeName() === 'HEAD') {
return false;
}
if (!event.dataTransfer || !treeElement.listItemElement.textContent) {
return;
}
event.dataTransfer.setData('text/plain', treeElement.listItemElement.textContent.replace(/\u200b/g, ''));
event.dataTransfer.effectAllowed = 'copyMove';
this._treeElementBeingDragged = treeElement;
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
return true;
}
_ondragover(event) {
if (!this._treeElementBeingDragged) {
return false;
}
const treeElement = this._validDragSourceOrTarget(this._treeElementFromEvent(event));
if (!treeElement) {
return false;
}
let node = treeElement.node();
while (node) {
if (node === this._treeElementBeingDragged._node) {
return false;
}
node = node.parentNode;
}
treeElement.listItemElement.classList.add('elements-drag-over');
this._dragOverTreeElement = treeElement;
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}
return false;
}
_ondragleave(event) {
this._clearDragOverTreeElementMarker();
event.preventDefault();
return false;
}
_validDragSourceOrTarget(treeElement) {
if (!treeElement) {
return null;
}
if (!(treeElement instanceof ElementsTreeElement)) {
return null;
}
const elementsTreeElement = treeElement;
const node = elementsTreeElement.node();
if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE) {
return null;
}
return elementsTreeElement;
}
_ondrop(event) {
event.preventDefault();
const treeElement = this._treeElementFromEvent(event);
if (treeElement instanceof ElementsTreeElement) {
this._doMove(treeElement);
}
}
_doMove(treeElement) {
if (!this._treeElementBeingDragged) {
return;
}
let parentNode;
let anchorNode;
if (treeElement.isClosingTag()) {
// Drop onto closing tag -> insert as last child.
parentNode = treeElement.node();
}
else {
const dragTargetNode = treeElement.node();
parentNode = dragTargetNode.parentNode;
anchorNode = dragTargetNode;
}
if (!parentNode || !anchorNode) {
return;
}
const wasExpanded = this._treeElementBeingDragged.expanded;
this._treeElementBeingDragged._node.moveTo(parentNode, anchorNode, this.selectNodeAfterEdit.bind(this, wasExpanded));
delete this._treeElementBeingDragged;
}
_ondragend(event) {
event.preventDefault();
this._clearDragOverTreeElementMarker();
delete this._treeElementBeingDragged;
}
_clearDragOverTreeElementMarker() {
if (this._dragOverTreeElement) {
this._dragOverTreeElement.listItemElement.classList.remove('elements-drag-over');
delete this._dragOverTreeElement;
}
}
_contextMenuEventFired(event) {
const treeElement = this._treeElementFromEvent(event);
if (treeElement instanceof ElementsTreeElement) {
this.showContextMenu(treeElement, event);
}
}
showContextMenu(treeElement, event) {
if (UI.UIUtils.isEditing()) {
return;
}
const contextMenu = new UI.ContextMenu.ContextMenu(event);
const isPseudoElement = Boolean(treeElement.node().pseudoType());
const isTag = treeElement.node().nodeType() === Node.ELEMENT_NODE && !isPseudoElement;
const node = event.target;
if (!node) {
return;
}
let textNode = node.enclosingNodeOrSelfWithClass('webkit-html-text-node');
if (textNode && textNode.classList.contains('bogus')) {
textNode = null;
}
const commentNode = node.enclosingNodeOrSelfWithClass('webkit-html-comment');
contextMenu.saveSection().appendItem(i18nString(UIStrings.storeAsGlobalVariable), this._saveNodeToTempVariable.bind(this, treeElement.node()));
if (textNode) {
treeElement.populateTextContextMenu(contextMenu, textNode);
}
else if (isTag) {
treeElement.populateTagContextMenu(contextMenu, event);
}
else if (commentNode) {
treeElement.populateNodeContextMenu(contextMenu);
}
else if (isPseudoElement) {
treeElement.populateScrollIntoView(contextMenu);
}
contextMenu.viewSection().appendItem(i18nString(UIStrings.adornerSettings), () => {
ElementsPanel.instance().showAdornerSettingsPane();
});
contextMenu.appendApplicableItems(treeElement.node());
contextMenu.show();
}
async _saveNodeToTempVariable(node) {
const remoteObjectForConsole = await node.resolveToObject();
await SDK.ConsoleModel.ConsoleModel.instance().saveToTempVariable(UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext), remoteObjectForConsole);
}
runPendingUpdates() {
this._updateModifiedNodes();
}
_onKeyDown(event) {
const keyboardEvent = event;
if (UI.UIUtils.isEditing()) {
return;
}
const node = this.selectedDOMNode();
if (!node) {
return;
}
const treeElement = this.treeElementByNode.get(node);
if (!treeElement) {
return;
}
if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(keyboardEvent) && node.parentNode) {
if (keyboardEvent.key === 'ArrowUp' && node.previousSibling) {
node.moveTo(node.parentNode, node.previousSibling, this.selectNodeAfterEdit.bind(this, treeElement.expanded));
keyboardEvent.consume(true);
return;
}
if (keyboardEvent.key === 'ArrowDown' && node.nextSibling) {
node.moveTo(node.parentNode, node.nextSibling.nextSibling, this.selectNodeAfterEdit.bind(this, treeElement.expanded));
keyboardEvent.consume(true);
return;
}
}
}
toggleEditAsHTML(node, startEditing, callback) {
const treeElement = this.treeElementByNode.get(node);
if (!treeElement || !treeElement.hasEditableNode()) {
return;
}
if (node.pseudoType()) {
return;
}
const parentNode = node.parentNode;
const index = node.index;
const wasExpanded = treeElement.expanded;
treeElement.toggleEditAsHTML(editingFinished.bind(this), startEditing);
function editingFinished(success) {
if (callback) {
callback();
}
if (!success) {
return;
}
// Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
this.runPendingUpdates();
if (!index) {
return;
}
const children = parentNode && parentNode.children();
const newNode = children ? children[index] || parentNode : parentNode;
if (!newNode) {
return;
}
this.selectDOMNode(newNode, true);
if (wasExpanded) {
const newTreeItem = this.findTreeElement(newNode);
if (newTreeItem) {
newTreeItem.expand();
}
}
}
}
selectNodeAfterEdit(wasExpanded, error, newNode) {
if (error) {
return null;
}
// Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
this.runPendingUpdates();
if (!newNode) {
return null;
}
this.selectDOMNode(newNode, true);
const newTreeItem = this.findTreeElement(newNode);
if (wasExpanded) {
if (newTreeItem) {
newTreeItem.expand();
}
}
return newTreeItem;
}
/**
* Runs a script on the node's remote object that toggles a class name on
* the node and injects a stylesheet into the head of the node's document
* containing a rule to set "visibility: hidden" on the class and all it's
* ancestors.
*/
async toggleHideElement(node) {
const pseudoType = node.pseudoType();
const effectiveNode = pseudoType ? node.parentNode : node;
if (!effectiveNode) {
return;
}
const hidden = node.marker('hidden-marker');
const object = await effectiveNode.resolveToObject('');
if (!object) {
return;
}
await object.callFunction(toggleClassAndInjectStyleRule, [{ value: pseudoType }, { value: !hidden }]);
object.release();
node.setMarker('hidden-marker', hidden ? null : true);
function toggleClassAndInjectStyleRule(pseudoType, hidden) {
const classNamePrefix = '__web-inspector-hide';
const classNameSuffix = '-shortcut__';
const styleTagId = '__web-inspector-hide-shortcut-style__';
const selectors = [];
selectors.push('.__web-inspector-hide-shortcut__');
selectors.push('.__web-inspector-hide-shortcut__ *');
selectors.push('.__web-inspector-hidebefore-shortcut__::before');
selectors.push('.__web-inspector-hideafter-shortcut__::after');
const selector = selectors.join(', ');
const ruleBody = ' visibility: hidden !important;';
const rule = '\n' + selector + '\n{\n' + ruleBody + '\n}\n';
const className = classNamePrefix + (pseudoType || '') + classNameSuffix;
this.classList.toggle(className, hidden);
let localRoot = this;
while (localRoot.parentNode) {
localRoot = localRoot.parentNode;
}
if (localRoot.nodeType === Node.DOCUMENT_NODE) {
localRoot = document.head;
}
let style = localRoot.querySelector('style#' + styleTagId);
if (style) {
return;
}
style = document.createElement('style');
style.id = styleTagId;
style.textContent = rule;
localRoot.appendChild(style);
}
}
isToggledToHidden(node) {
return Boolean(node.marker('hidden-marker'));
}
_reset() {
this.rootDOMNode = null;
this.selectDOMNode(null, false);
this._imagePreviewPopover.hide();
delete this._clipboardNodeData;
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
this._updateRecords.clear();
}
wireToDOMModel(domModel) {
elementsTreeOutlineByDOMModel.set(domModel, this);
domModel.addEventListener(SDK.DOMModel.Events.MarkersChanged, this._markersChanged, this);
domModel.addEventListener(SDK.DOMModel.Events.NodeInserted, this._nodeInserted, this);
domModel.addEventListener(SDK.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
domModel.addEventListener(SDK.DOMModel.Events.AttrModified, this._attributeModified, this);
domModel.addEventListener(SDK.DOMModel.Events.AttrRemoved, this._attributeRemoved, this);
domModel.addEventListener(SDK.DOMModel.Events.CharacterDataModified, this._characterDataModified, this);
domModel.addEventListener(SDK.DOMModel.Events.DocumentUpdated, this._documentUpdated, this);
domModel.addEventListener(SDK.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
domModel.addEventListener(SDK.DOMModel.Events.DistributedNodesChanged, this._distributedNodesChanged, this);
}
unwireFromDOMModel(domModel) {
domModel.removeEventListener(SDK.DOMModel.Events.MarkersChanged, this._markersChanged, this);
domModel.removeEventListener(SDK.DOMModel.Events.NodeInserted, this._nodeInserted, this);
domModel.removeEventListener(SDK.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
domModel.removeEventListener(SDK.DOMModel.Events.AttrModified, this._attributeModified, this);
domModel.removeEventListener(SDK.DOMModel.Events.AttrRemoved, this._attributeRemoved, this);
domModel.removeEventListener(SDK.DOMModel.Events.CharacterDataModified, this._characterDataModified, this);
domModel.removeEventListener(SDK.DOMModel.Events.DocumentUpdated, this._documentUpdated, this);
domModel.removeEventListener(SDK.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
domModel.removeEventListener(SDK.DOMModel.Events.DistributedNodesChanged, this._distributedNodesChanged, this);
elementsTreeOutlineByDOMModel.delete(domModel);
}
_addUpdateRecord(node) {
let record = this._updateRecords.get(node);
if (!record) {
record = new UpdateRecord();
this._updateRecords.set(node, record);
}
return record;
}
_updateRecordForHighlight(node) {
if (!this._visible) {
return null;
}
return this._updateRecords.get(node) || null;
}
_documentUpdated(event) {
const domModel = event.data;
this._reset();
if (domModel.existingDocument()) {
this.rootDOMNode = domModel.existingDocument();
}
}
_attributeModified(event) {
const node = event.data.node;
this._addUpdateRecord(node).attributeModified(event.data.name);
this._updateModifiedNodesSoon();
}
_attributeRemoved(event) {
const node = event.data.node;
this._addUpdateRecord(node).attributeRemoved(event.data.name);
this._updateModifiedNodesSoon();
}
_characterDataModified(event) {
const node = event.data;
this._addUpdateRecord(node).charDataModified();
// Text could be large and force us to render itself as the child in the tree outline.
if (node.parentNode && node.parentNode.firstChild === node.parentNode.lastChild) {
this._addUpdateRecord(node.parentNode).childrenModified();
}
this._updateModifiedNodesSoon();
}
_nodeInserted(event) {
const node = event.data;
this._addUpdateRecord(node.parentNode).nodeInserted(node);
this._updateModifiedNodesSoon();
}
_nodeRemoved(event) {
const node = event.data.node;
const parentNode = event.data.parent;
this.resetClipboardIfNeeded(node);
this._addUpdateRecord(parentNode).nodeRemoved(node);
this._updateModifiedNodesSoon();
}
_childNodeCountUpdated(event) {
const node = event.data;
this._addUpdateRecord(node).childrenModified();
this._updateModifiedNodesSoon();
}
_distributedNodesChanged(event) {
const node = event.data;
this._addUpdateRecord(node).childrenModified();
this._updateModifiedNodesSoon();
}
_updateModifiedNodesSoon() {
if (!this._updateRecords.size) {
return;
}
if (this._updateModifiedNodesTimeout) {
return;
}
this._updateModifiedNodesTimeout = window.setTimeout(this._updateModifiedNodes.bind(this), 50);
}
_updateModifiedNodes() {
if (this._updateModifiedNodesTimeout) {
clearTimeout(this._updateModifiedNodesTimeout);
delete this._updateModifiedNodesTimeout;
}
const updatedNodes = [...this._updateRecords.keys()];
const hidePanelWhileUpdating = updatedNodes.length > 10;
let treeOutlineContainerElement;
let originalScrollTop;
if (hidePanelWhileUpdating) {
treeOutlineContainerElement = this.element.parentNode;
originalScrollTop = treeOutlineContainerElement ? treeOutlineContainerElement.scrollTop : 0;
this._element.classList.add('hidden');
}
const rootNodeUpdateRecords = this._rootDOMNode && this._updateRecords.get(this._rootDOMNode);
if (rootNodeUpdateRecords && rootNodeUpdateRecords.hasChangedChildren()) {
// Document's children have changed, perform total update.
this.update();
}
else {
for (const [node, record] of this._updateRecords) {
if (record.hasChangedChildren()) {
this._updateModifiedParentNode(node);
}
else {
this._updateModifiedNode(node);
}
}
}
if (hidePanelWhileUpdating) {
this._element.classList.remove('hidden');
if (treeOutlineContainerElement && originalScrollTop) {
treeOutlineContainerElement.scrollTop = originalScrollTop;
}
}
this._updateRecords.clear();
this._fireElementsTreeUpdated(updatedNodes);
}
_updateModifiedNode(node) {
const treeElement = this.findTreeElement(node);
if (treeElement) {
treeElement.updateTitle(this._updateRecordForHighlight(node));
}
}
_updateModifiedParentNode(node) {
const parentTreeElement = this.findTreeElement(node);
if (parentTreeElement) {
parentTreeElement.setExpandable(this._hasVisibleChildren(node));
parentTreeElement.updateTitle(this._updateRecordForHighlight(node));
if (populatedTreeElements.has(parentTreeElement)) {
this._updateChildren(parentTreeElement);
}
}
}
populateTreeElement(treeElement) {
if (treeElement.childCount() || !treeElement.isExpandable()) {
return Promise.resolve();
}
return new Promise(resolve => {
treeElement.node().getChildNodes(() => {
populatedTreeElements.add(treeElement);
this._updateModifiedParentNode(treeElement.node());
resolve();
});
});
}
_createElementTreeElement(node, isClosingTag) {
const treeElement = new ElementsTreeElement(node, isClosingTag);
treeElement.setExpandable(!isClosingTag && this._hasVisibleChildren(node));
if (node.nodeType() === Node.ELEMENT_NODE && node.parentNode && node.parentNode.nodeType() === Node.DOCUMENT_NODE &&
!node.parentNode.parentNode) {
treeElement.setCollapsible(false);
}
treeElement.selectable = Boolean(this._selectEnabled);
return treeElement;
}
_showChild(treeElement, child) {
if (treeElement.isClosingTag()) {
return null;
}
const index = this._visibleChildren(treeElement.node()).indexOf(child);
if (index === -1) {
return null;
}
if (index >= treeElement.expandedChildrenLimit()) {
this.setExpandedChildrenLimit(treeElement, index + 1);
}
return /** @type {!ElementsTreeElement} */ treeElement.childAt(index);
}
_visibleChildren(node) {
let visibleChildren = ElementsTreeElement.visibleShadowRoots(node);
const contentDocument = node.contentDocument();
if (contentDocument) {
visibleChildren.push(contentDocument);
}
const templateContent = node.templateContent();
if (templateContent) {
visibleChildren.push(templateContent);
}
const markerPseudoElement = node.markerPseudoElement();
if (markerPseudoElement) {
visibleChildren.push(markerPseudoElement);
}
const beforePseudoElement = node.beforePseudoElement();
if (beforePseudoElement) {
visibleChildren.push(beforePseudoElement);
}
if (node.childNodeCount()) {
// Children may be stale when the outline is not wired to receive DOMModel updates.
let children = node.children() || [];
if (!this._showHTMLCommentsSetting.get()) {
children = children.filter(n => n.nodeType() !== Node.COMMENT_NODE);
}
visibleChildren = visibleChildren.concat(children);
}
const afterPseudoElement = node.afterPseudoElement();
if (afterPseudoElement) {
visibleChildren.push(afterPseudoElement);
}
return visibleChildren;
}
_hasVisibleChildren(node) {
if (node.isIframe()) {
return true;
}
if (node.isPortal()) {
return true;
}
if (node.contentDocument()) {
return true;
}
if (node.templateContent()) {
return true;
}
if (ElementsTreeElement.visibleShadowRoots(node).length) {
return true;
}
if (node.hasPseudoElements()) {
return true;
}
if (node.isInsertionPoint()) {
return true;
}
return Boolean(node.childNodeCount()) && !ElementsTreeElement.canShowInlineText(node);
}
_createExpandAllButtonTreeElement(treeElement) {
const button = UI.UIUtils.createTextButton('', handleLoadAllChildren.bind(this));
button.value = '';
const expandAllButtonElement = new UI.TreeOutline.TreeElement(button);
expandAllButtonElement.selectable = false;
expandAllButtonElement.button = button;
return expandAllButtonElement;
function handleLoadAllChildren(event) {
const visibleChildCount = this._visibleChildren(treeElement.node()).length;
this.setExpandedChildrenLimit(treeElement, Math.max(visibleChildCount, treeElement.expandedChildrenLimit() + InitialChildrenLimit));
event.consume();
}
}
setExpandedChildrenLimit(treeElement, expandedChildrenLimit) {
if (treeElement.expandedChildrenLimit() === expandedChildrenLimit) {
return;
}
treeElement.setExpandedChildrenLimit(expandedChildrenLimit);
if (treeElement.treeOutline && !this._treeElementsBeingUpdated.has(treeElement)) {
this._updateModifiedParentNode(treeElement.node());
}
}
_updateChildren(treeElement) {
if (!treeElement.isExpandable()) {
if (!treeElement.treeOutline) {
return;
}
const selectedTreeElement = treeElement.treeOutline.selectedTreeElement;
if (selectedTreeElement && selectedTreeElement.hasAncestor(treeElement)) {
treeElement.select(true);
}
treeElement.removeChildren();
return;
}
console.assert(!treeElement.isClosingTag());
this._innerUpdateChildren(treeElement);
}
insertChildElement(treeElement, child, index, isClosingTag) {
const newElement = this._createElementTreeElement(child, isClosingTag);
treeElement.insertChild(newElement, index);
return newElement;
}
_moveChild(treeElement, child, targetIndex) {
if (treeElement.indexOfChild(child) === targetIndex) {
return;
}
const wasSelected = child.selected;
if (child.parent) {
child.parent.removeChild(child);
}
treeElement.insertChild(child, targetIndex);
if (wasSelected) {
child.select();
}
}
_innerUpdateChildren(treeElement) {
if (this._treeElementsBeingUpdated.has(treeElement)) {
return;
}
this._treeElementsBeingUpdated.add(treeElement);
const node = treeElement.node();
const visibleChildren = this._visibleChildren(node);
const visibleChildrenSet = new Set(visibleChildren);
// Remove any tree elements that no longer have this node as their parent and save
// all existing elements that could be reused. This also removes closing tag element.
const existingTreeElements = new Map();
for (let i = treeElement.childCount() - 1; i >= 0; --i) {
const existingTreeElement = treeElement.childAt(i);
if (!(existingTreeElement instanceof ElementsTreeElement)) {
// Remove expand all button and shadow host toolbar.
treeElement.removeChildAtIndex(i);
continue;
}
const elementsTreeElement = existingTreeElement;
const existingNode = elementsTreeElement.node();
if (visibleChildrenSet.has(existingNode)) {
existingTreeElements.set(existingNode, existingTreeElement);
continue;
}
treeElement.removeChildAtIndex(i);
}
for (let i = 0; i < visibleChildren.length && i < treeElement.expandedChildrenLimit(); ++i) {
const child = visibleChildren[i];
const existingTreeElement = existingTreeElements.get(child) || this.findTreeElement(child);
if (existingTreeElement && existingTreeElement !== treeElement) {
// If an existing element was found, just move it.
this._moveChild(treeElement, existingTreeElement, i);
}
else {
// No existing element found, insert a new element.
const newElement = this.insertChildElement(treeElement, child, i);
if (this._updateRecordForHighlight(node) && treeElement.expanded) {
ElementsTreeElement.animateOnDOMUpdate(newElement);
}
// If a node was inserted in the middle of existing list dynamically we might need to increase the limit.
if (treeElement.childCount() > treeElement.expandedChildrenLimit()) {
this.setExpandedChildrenLimit(treeElement, treeElement.expandedChildrenLimit() + 1);
}
}
}
// Update expand all button.
const expandedChildCount = treeElement.childCount();
if (visibleChildren.length > expandedChildCount) {
const targetButtonIndex = expandedChildCount;
if (!treeElement.expandAllButtonElement) {
treeElement.expandAllButtonElement = this._createExpandAllButtonTreeElement(treeElement);
}
treeElement.insertChild(treeElement.expandAllButtonElement, targetButtonIndex);
treeElement.expandAllButtonElement.title =
i18nString(UIStrings.showAllNodesDMore, { PH1: visibleChildren.length - expandedChildCount });
}
else if (treeElement.expandAllButtonElement) {
treeElement.expandAllButtonElement = null;
}
// Insert shortcuts to distributed children.
if (node.isInsertionPoint()) {
for (const distributedNode of node.distributedNodes()) {
treeElement.appendChild(new ShortcutTreeElement(distributedNode));
}
}
// Insert close tag.
if (node.nodeType() === Node.ELEMENT_NODE && !node.pseudoType() && treeElement.isExpandable()) {
this.insertChildElement(treeElement, node, treeElement.childCount(), true);
}
this._treeElementsBeingUpdated.delete(treeElement);
}
_markersChanged(event) {
const node = event.data;
const treeElement = this.treeElementByNode.get(node);
if (treeElement) {
treeElement.updateDecorations();
}
}
static _treeOutlineSymbol = Symbol('treeOutline');
}
(function (ElementsTreeOutline) {
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
let Events;
(function (Events) {
Events["SelectedNodeChanged"] = "SelectedNodeChanged";
Events["ElementsTreeUpdated"] = "ElementsTreeUpdated";
})(Events = ElementsTreeOutline.Events || (ElementsTreeOutline.Events = {}));
})(ElementsTreeOutline || (ElementsTreeOutline = {}));
// clang-format off
export const MappedCharToEntity = new Map([
['\xA0', 'nbsp'],
['\xAD', 'shy'],
['\u2002', 'ensp'],
['\u2003', 'emsp'],
['\u2009', 'thinsp'],
['\u200A', 'hairsp'],
['\u200B', 'ZeroWidthSpace'],
['\u200C', 'zwnj'],
['\u200D', 'zwj'],
['