UNPKG

chrome-devtools-frontend

Version:
1,310 lines (1,203 loc) • 119 kB
// Copyright 2021 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-imperative-dom-api */ /* * 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. */ import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import * as Badges from '../../models/badges/badges.js'; import type * as Elements from '../../models/elements/elements.js'; import type * as IssuesManager from '../../models/issues_manager/issues_manager.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js'; import type {DirectiveResult} from '../../third_party/lit/lib/directive.js'; import * as Adorners from '../../ui/components/adorners/adorners.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import * as CodeHighlighter from '../../ui/components/code_highlighter/code_highlighter.js'; import * as Highlighting from '../../ui/components/highlighting/highlighting.js'; import * as TextEditor from '../../ui/components/text_editor/text_editor.js'; import {createIcon, Icon} from '../../ui/kit/kit.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Lit from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import * as PanelsCommon from '../common/common.js'; import * as Emulation from '../emulation/emulation.js'; import * as Media from '../media/media.js'; import * as ElementsComponents from './components/components.js'; import {canGetJSPath, cssPath, jsPath, xPath} from './DOMPath.js'; import {getElementIssueDetails} from './ElementIssueUtils.js'; import {ElementsPanel} from './ElementsPanel.js'; import {type ElementsTreeOutline, MappedCharToEntity} from './ElementsTreeOutline.js'; import {ImagePreviewPopover} from './ImagePreviewPopover.js'; import {getRegisteredDecorators, type MarkerDecorator, type MarkerDecoratorRegistration} from './MarkerDecorator.js'; const {html, nothing, render, Directives: {ref, repeat}} = Lit; const UIStrings = { /** * @description Title for Ad adorner. This iframe is marked as advertisement frame. */ thisFrameWasIdentifiedAsAnAd: 'This frame was identified as an ad frame', /** * @description A context menu item in the Elements panel. Force is used as a verb, indicating intention to make the state change. */ forceState: 'Force state', /** * @description Hint element title in Elements Tree Element of the Elements panel * @example {0} PH1 */ useSInTheConsoleToReferToThis: 'Use {PH1} in the console to refer to this element.', /** * @description A context menu item in the Elements Tree Element of the Elements panel */ addAttribute: 'Add attribute', /** * @description Text to modify the attribute of an item */ editAttribute: 'Edit attribute', /** * @description Text to focus on something */ focus: 'Focus', /** * @description Text to scroll the displayed content into view */ scrollIntoView: 'Scroll into view', /** * @description A context menu item in the Elements Tree Element of the Elements panel */ editText: 'Edit text', /** * @description A context menu item in the Elements Tree Element of the Elements panel */ editAsHtml: 'Edit as HTML', /** * @description Text to cut an element, cut should be used as a verb */ cut: 'Cut', /** * @description Text for copying, copy should be used as a verb */ copy: 'Copy', /** * @description Text to paste an element, paste should be used as a verb */ paste: 'Paste', /** * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb */ copyOuterhtml: 'Copy outerHTML', /** * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb */ copySelector: 'Copy `selector`', /** * @description Text in Elements Tree Element of the Elements panel */ copyJsPath: 'Copy JS path', /** * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb */ copyStyles: 'Copy styles', /** * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb */ copyXpath: 'Copy XPath', /** * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb */ copyFullXpath: 'Copy full XPath', /** * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb */ copyElement: 'Copy element', /** * @description A context menu item in the Elements Tree Element of the Elements panel */ duplicateElement: 'Duplicate element', /** * @description Text to hide an element */ hideElement: 'Hide element', /** * @description A context menu item in the Elements Tree Element of the Elements panel */ deleteElement: 'Delete element', /** * @description Text to expand something recursively */ expandRecursively: 'Expand recursively', /** * @description Text to collapse children of a parent group */ collapseChildren: 'Collapse children', /** * @description Title of an action in the emulation tool to capture node screenshot */ captureNodeScreenshot: 'Capture node screenshot', /** * @description Title of a context menu item. When clicked DevTools goes to the Application panel and shows this specific iframe's details */ showFrameDetails: 'Show `iframe` details', /** * @description Text in Elements Tree Element of the Elements panel */ valueIsTooLargeToEdit: '<value is too large to edit>', /** * @description Element text content in Elements Tree Element of the Elements panel */ children: 'Children:', /** * @description ARIA label for Elements Tree adorners */ enableGridMode: 'Enable grid mode', /** * @description ARIA label for Elements Tree adorners */ disableGridMode: 'Disable grid mode', /** * @description ARIA label for Elements Tree adorners */ /** * @description ARIA label for Elements Tree adorners */ enableGridLanesMode: 'Enable grid-lanes mode', /** * @description ARIA label for Elements Tree adorners */ disableGridLanesMode: 'Disable grid-lanes mode', /** * @description ARIA label for an elements tree adorner */ forceOpenPopover: 'Keep this popover open', /** * @description ARIA label for an elements tree adorner */ stopForceOpenPopover: 'Stop keeping this popover open', /** * @description Label of the adorner for flex elements in the Elements panel */ enableFlexMode: 'Enable flex mode', /** * @description Label of the adorner for flex elements in the Elements panel */ disableFlexMode: 'Disable flex mode', /** * @description Label of an adorner in the Elements panel. When clicked, it enables * the overlay showing CSS scroll snapping for the current element. */ enableScrollSnap: 'Enable scroll-snap overlay', /** * @description Label of an adorner in the Elements panel. When clicked, it disables * the overlay showing CSS scroll snapping for the current element. */ disableScrollSnap: 'Disable scroll-snap overlay', /** * @description Label of an adorner in the Elements panel. When clicked, it enables * the overlay showing the container overlay for the current element. */ enableContainer: 'Enable container overlay', /** * @description Label of an adorner in the Elements panel. When clicked, it disables * the overlay showing container for the current element. */ disableContainer: 'Disable container overlay', /** * @description Label of an adorner in the Elements panel. When clicked, it forces * the element into applying its starting-style rules. */ enableStartingStyle: 'Enable @starting-style mode', /** * @description Label of an adorner in the Elements panel. When clicked, it no longer * forces the element into applying its starting-style rules. */ disableStartingStyle: 'Disable @starting-style mode', /** * @description Label of an adorner in the Elements panel. When clicked, it redirects * to the Media Panel. */ openMediaPanel: 'Jump to Media panel', /** * @description Text of a tooltip to redirect to another element in the Elements panel */ showPopoverTarget: 'Show element associated with the `popovertarget` attribute', /** * @description Text of a tooltip to redirect to another element in the Elements panel, associated with the `interesttarget` attribute */ showInterestTarget: 'Show element associated with the `interesttarget` attribute', /** * @description Text of a tooltip to redirect to another element in the Elements panel, associated with the `commandfor` attribute */ showCommandForTarget: 'Show element associated with the `commandfor` attribute', /** * @description Text of the tooltip for scroll adorner. */ elementHasScrollableOverflow: 'This element has a scrollable overflow', /** * @description Text of a context menu item to redirect to the AI assistance panel and to start a chat. */ startAChat: 'Start a chat', /** * @description Context menu item in Elements panel to assess visibility of an element via AI. */ assessVisibility: 'Assess visibility', /** * @description Context menu item in Elements panel to center an element via AI. */ centerElement: 'Center element', /** * @description Context menu item in Elements panel to wrap flex items via AI. */ wrapTheseItems: 'Wrap these items', /** * @description Context menu item in Elements panel to distribute flex items evenly via AI. */ distributeItemsEvenly: 'Distribute items evenly', /** * @description Context menu item in Elements panel to explain flexbox via AI. */ explainFlexbox: 'Explain flexbox', /** * @description Context menu item in Elements panel to align grid items via AI. */ alignItems: 'Align items', /** * @description Context menu item in Elements panel to add padding/gap to grid via AI. */ addPadding: 'Add padding', /** * @description Context menu item in Elements panel to explain grid layout via AI. */ explainGridLayout: 'Explain grid layout', /** * @description Context menu item in Elements panel to find grid definition for a subgrid item via AI. */ findGridDefinition: 'Find grid definition', /** * @description Context menu item in Elements panel to change parent grid properties for a subgrid item via AI. */ changeParentProperties: 'Change parent properties', /** * @description Context menu item in Elements panel to explain subgrids via AI. */ explainSubgrids: 'Explain subgrids', /** * @description Context menu item in Elements panel to remove scrollbars via AI. */ removeScrollbars: 'Remove scrollbars', /** * @description Context menu item in Elements panel to style scrollbars via AI. */ styleScrollbars: 'Style scrollbars', /** * @description Context menu item in Elements panel to explain scrollbars via AI. */ explainScrollbars: 'Explain scrollbars', /** * @description Context menu item in Elements panel to explain container queries via AI. */ explainContainerQueries: 'Explain container queries', /** * @description Context menu item in Elements panel to explain container types via AI. */ explainContainerTypes: 'Explain container types', /** * @description Context menu item in Elements panel to explain container context via AI. */ explainContainerContext: 'Explain container context', /** * @description Link text content in Elements Tree Outline of the Elements panel. When clicked, it "reveals" the true location of an element. */ reveal: 'reveal', } as const; const str_ = i18n.i18n.registerUIStrings('panels/elements/ElementsTreeElement.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const enum TagType { OPENING = 'OPENING_TAG', CLOSING = 'CLOSING_TAG', } interface OpeningTagContext { tagType: TagType.OPENING; canAddAttributes: boolean; } interface ClosingTagContext { tagType: TagType.CLOSING; } export type TagTypeContext = OpeningTagContext|ClosingTagContext; export function isOpeningTag(context: TagTypeContext): context is OpeningTagContext { return context.tagType === TagType.OPENING; } export interface ViewInput { containerAdornerActive: boolean; flexAdornerActive: boolean; gridAdornerActive: boolean; popoverAdornerActive: boolean; showAdAdorner: boolean; showContainerAdorner: boolean; showFlexAdorner: boolean; showGridAdorner: boolean; showGridLanesAdorner: boolean; showMediaAdorner: boolean; showPopoverAdorner: boolean; showTopLayerAdorner: boolean; isSubgrid: boolean; adorners?: Set<Adorners.Adorner.Adorner>; nodeInfo?: DocumentFragment; topLayerIndex: number; onGutterClick: (e: Event) => void; onAdornerAdded: (adorner: Adorners.Adorner.Adorner) => void; onAdornerRemoved: (adorner: Adorners.Adorner.Adorner) => void; onContainerAdornerClick: (e: Event) => void; onFlexAdornerClick: (e: Event) => void; onGridAdornerClick: (e: Event) => void; onMediaAdornerClick: (e: Event) => void; onPopoverAdornerClick: (e: Event) => void; onTopLayerAdornerClick: (e: Event) => void; } export interface ViewOutput { gutterContainer?: HTMLElement; decorationsElement?: HTMLElement; contentElement?: HTMLElement; } function adornerRef(input: ViewInput): DirectiveResult<typeof Lit.Directives.RefDirective> { let adorner: Adorners.Adorner.Adorner|undefined; return ref((el?: Element) => { if (adorner) { input.onAdornerRemoved(adorner); } adorner = el as Adorners.Adorner.Adorner; if (adorner) { if (ElementsPanel.instance().isAdornerEnabled(adorner.name)) { adorner.show(); } else { adorner.hide(); } input.onAdornerAdded(adorner); } }); } export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLElement): void => { const adAdornerConfig = ElementsComponents.AdornerManager.getRegisteredAdorner(ElementsComponents.AdornerManager.RegisteredAdorners.AD); const containerAdornerConfig = ElementsComponents.AdornerManager.getRegisteredAdorner( ElementsComponents.AdornerManager.RegisteredAdorners.CONTAINER); const flexAdornerConfig = ElementsComponents.AdornerManager.getRegisteredAdorner(ElementsComponents.AdornerManager.RegisteredAdorners.FLEX); const gridAdornerConfig = ElementsComponents.AdornerManager.getRegisteredAdorner(ElementsComponents.AdornerManager.RegisteredAdorners.GRID); const subgridAdornerConfig = ElementsComponents.AdornerManager.getRegisteredAdorner( ElementsComponents.AdornerManager.RegisteredAdorners.SUBGRID); const gridLanesAdornerConfig = ElementsComponents.AdornerManager.getRegisteredAdorner( ElementsComponents.AdornerManager.RegisteredAdorners.GRID_LANES); const mediaAdornerConfig = ElementsComponents.AdornerManager.getRegisteredAdorner( ElementsComponents.AdornerManager.RegisteredAdorners.MEDIA); const popoverAdornerConfig = ElementsComponents.AdornerManager.getRegisteredAdorner( ElementsComponents.AdornerManager.RegisteredAdorners.POPOVER); const topLayerAdornerConfig = ElementsComponents.AdornerManager.getRegisteredAdorner( ElementsComponents.AdornerManager.RegisteredAdorners.TOP_LAYER); const hasAdorners = input.adorners?.size || input.showAdAdorner || input.showContainerAdorner || input.showFlexAdorner || input.showGridAdorner || input.showGridLanesAdorner || input.showMediaAdorner || input.showPopoverAdorner || input.showTopLayerAdorner; // clang-format off render(html` <div ${ref(el => { output.contentElement = el as HTMLElement; })}> ${input.nodeInfo ? html`<span class="highlight">${input.nodeInfo}</span>` : nothing} <div class="gutter-container" @click=${input.onGutterClick} ${ref(el => { output.gutterContainer = el as HTMLElement; })}> <devtools-icon name="dots-horizontal"></devtools-icon> <div class="hidden" ${ref(el => { output.decorationsElement = el as HTMLElement; })}></div> </div> ${hasAdorners ? html`<div class="adorner-container ${!hasAdorners ? 'hidden' : ''}"> ${input.showAdAdorner ? html`<devtools-adorner aria-label=${i18nString(UIStrings.thisFrameWasIdentifiedAsAnAd)} .data=${{name: adAdornerConfig.name, jslogContext: adAdornerConfig.name}} ${adornerRef(input)}> <span>${adAdornerConfig.name}</span> </devtools-adorner>` : nothing} ${input.showContainerAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .data=${{name: containerAdornerConfig.name, jslogContext: containerAdornerConfig.name}} jslog=${VisualLogging.adorner(containerAdornerConfig.name).track({click: true})} active=${input.containerAdornerActive} aria-label=${input.containerAdornerActive ? i18nString(UIStrings.enableContainer) : i18nString(UIStrings.disableContainer)} @click=${input.onContainerAdornerClick} @keydown=${(event: KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'Space') { input.onContainerAdornerClick(event); event.stopPropagation(); } }} ${adornerRef(input)}> <span>${containerAdornerConfig.name}</span> </devtools-adorner>`: nothing} ${input.showFlexAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .data=${{name: flexAdornerConfig.name, jslogContext: flexAdornerConfig.name}} jslog=${VisualLogging.adorner(flexAdornerConfig.name).track({click: true})} active=${input.flexAdornerActive} aria-label=${input.flexAdornerActive ? i18nString(UIStrings.disableFlexMode) : i18nString(UIStrings.enableFlexMode)} @click=${input.onFlexAdornerClick} @keydown=${(event: KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'Space') { input.onFlexAdornerClick(event); event.stopPropagation(); } }} ${adornerRef(input)}> <span>${flexAdornerConfig.name}</span> </devtools-adorner>`: nothing} ${input.showGridAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .data=${{ name: input.isSubgrid ? subgridAdornerConfig.name : gridAdornerConfig.name, jslogContext: input.isSubgrid ? subgridAdornerConfig.name : gridAdornerConfig.name, }} jslog=${VisualLogging.adorner(input.isSubgrid ? subgridAdornerConfig.name : gridAdornerConfig.name).track({click: true})} active=${input.gridAdornerActive} aria-label=${input.gridAdornerActive ? i18nString(UIStrings.disableGridMode) : i18nString(UIStrings.enableGridMode)} @click=${input.onGridAdornerClick} @keydown=${(event: KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'Space') { input.onGridAdornerClick(event); event.stopPropagation(); } }} ${adornerRef(input)}> <span>${input.isSubgrid ? subgridAdornerConfig.name : gridAdornerConfig.name}</span> </devtools-adorner>`: nothing} ${input.showGridLanesAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .data=${{name: gridLanesAdornerConfig.name, jslogContext: gridLanesAdornerConfig.name}} jslog=${VisualLogging.adorner(gridLanesAdornerConfig.name).track({click: true})} active=${input.gridAdornerActive} aria-label=${input.gridAdornerActive ? i18nString(UIStrings.disableGridLanesMode) : i18nString(UIStrings.enableGridLanesMode)} @click=${input.onGridAdornerClick} @keydown=${(event: KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'Space') { input.onGridAdornerClick(event); event.stopPropagation(); } }} ${adornerRef(input)}> <span>${gridLanesAdornerConfig.name}</span> </devtools-adorner>`: nothing} ${input.showMediaAdorner ? html`<devtools-adorner class=clickable role=button tabindex=0 .data=${{name: mediaAdornerConfig.name, jslogContext: mediaAdornerConfig.name}} jslog=${VisualLogging.adorner(mediaAdornerConfig.name).track({click: true})} aria-label=${i18nString(UIStrings.openMediaPanel)} @click=${input.onMediaAdornerClick} @keydown=${(event: KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'Space') { input.onMediaAdornerClick(event); event.stopPropagation(); } }} ${adornerRef(input)}> <span class="adorner-with-icon"> ${mediaAdornerConfig.name}<devtools-icon name="select-element"></devtools-icon> </span> </devtools-adorner>`: nothing} ${input.showPopoverAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .data=${{name: popoverAdornerConfig.name, jslogContext: popoverAdornerConfig.name}} jslog=${VisualLogging.adorner(popoverAdornerConfig.name).track({click: true})} active=${input.popoverAdornerActive} aria-label=${input.popoverAdornerActive ? i18nString(UIStrings.stopForceOpenPopover) : i18nString(UIStrings.forceOpenPopover)} @click=${input.onPopoverAdornerClick} @keydown=${(event: KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'Space') { input.onPopoverAdornerClick(event); event.stopPropagation(); } }} ${adornerRef(input)}> <span>${popoverAdornerConfig.name}</span> </devtools-adorner>`: nothing} ${input.showTopLayerAdorner ? html`<devtools-adorner class=clickable role=button tabindex=0 .data=${{name: topLayerAdornerConfig.name, jslogContext: topLayerAdornerConfig.name}} jslog=${VisualLogging.adorner(topLayerAdornerConfig.name).track({click: true})} aria-label=${i18nString(UIStrings.reveal)} @click=${input.onTopLayerAdornerClick} @keydown=${(event: KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'Space') { input.onTopLayerAdornerClick(event); event.stopPropagation(); } }} ${adornerRef(input)}> <span class="adorner-with-icon"> ${`top-layer (${input.topLayerIndex})`}<devtools-icon name="select-element"></devtools-icon> </span> </devtools-adorner>`: nothing} ${repeat(Array.from((input.adorners ?? new Set()).values()).sort(adornerComparator), adorner => { return adorner; })} </div>`: nothing} </div> `, target); // clang-format on }; export class ElementsTreeElement extends UI.TreeOutline.TreeElement { nodeInternal: SDK.DOMModel.DOMNode; override treeOutline: ElementsTreeOutline|null; // Handled by the view output for now. gutterContainer!: HTMLElement; decorationsElement!: HTMLElement; contentElement!: HTMLElement; private searchQuery: string|null; #expandedChildrenLimit: number; private readonly decorationsThrottler: Common.Throttler.Throttler; private inClipboard: boolean; #hovered: boolean; private editing: EditorHandles|null; private htmlEditElement?: HTMLElement; expandAllButtonElement: UI.TreeOutline.TreeElement|null; selectionElement?: HTMLDivElement; private hintElement?: HTMLElement; private aiButtonContainer?: HTMLElement; #elementIssues = new Map<string, IssuesManager.Issue.Issue>(); #nodeElementToIssue = new Map<Element, IssuesManager.Issue.Issue[]>(); #highlights: Range[] = []; readonly tagTypeContext: TagTypeContext; #adornersThrottler = new Common.Throttler.Throttler(100); #adorners = new Set<Adorners.Adorner.Adorner>(); #nodeInfo?: DocumentFragment; #containerAdornerActive = false; #flexAdornerActive = false; #gridAdornerActive = false; #popoverAdornerActive = false; #layout: SDK.CSSModel.LayoutProperties|null = null; constructor(node: SDK.DOMModel.DOMNode, isClosingTag?: boolean) { // The title will be updated in onattach. super(); this.nodeInternal = node; this.treeOutline = null; this.listItemElement.setAttribute( 'jslog', `${VisualLogging.treeItem().parent('elementsTreeOutline').track({ keydown: 'ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Backspace|Delete|Enter|Space|Home|End', drag: true, click: true, })}`); this.searchQuery = null; this.#expandedChildrenLimit = InitialChildrenLimit; this.decorationsThrottler = new Common.Throttler.Throttler(100); this.inClipboard = false; this.#hovered = false; this.editing = null; if (isClosingTag) { this.tagTypeContext = {tagType: TagType.CLOSING}; } else { this.tagTypeContext = { tagType: TagType.OPENING, canAddAttributes: this.nodeInternal.nodeType() === Node.ELEMENT_NODE, }; void this.updateStyleAdorners(); void this.updateScrollAdorner(); void this.#updateAdorners(); } this.expandAllButtonElement = null; this.performUpdate(); if (this.nodeInternal.retained && !this.isClosingTag()) { const icon = new Icon(); icon.name = 'small-status-dot'; icon.style.color = 'var(--icon-error)'; icon.classList.add('extra-small'); icon.style.setProperty('vertical-align', 'middle'); this.setLeadingIcons([icon]); this.listItemNode.classList.add('detached-elements-detached-node'); this.listItemNode.style.setProperty('display', '-webkit-box'); this.listItemNode.setAttribute('title', 'Retained Node'); } if (this.nodeInternal.detached && !this.isClosingTag()) { this.listItemNode.setAttribute('title', 'Detached Tree Node'); } node.domModel().overlayModel().addEventListener( SDK.OverlayModel.Events.PERSISTENT_CONTAINER_QUERY_OVERLAY_STATE_CHANGED, event => { const {nodeId: eventNodeId, enabled} = event.data; if (eventNodeId !== node.id) { return; } this.#containerAdornerActive = enabled; this.performUpdate(); }); node.domModel().overlayModel().addEventListener( SDK.OverlayModel.Events.PERSISTENT_FLEX_CONTAINER_OVERLAY_STATE_CHANGED, event => { const {nodeId: eventNodeId, enabled} = event.data; if (eventNodeId !== node.id) { return; } this.#flexAdornerActive = enabled; this.performUpdate(); }); node.domModel().overlayModel().addEventListener( SDK.OverlayModel.Events.PERSISTENT_GRID_OVERLAY_STATE_CHANGED, event => { const {nodeId: eventNodeId, enabled} = event.data; if (eventNodeId !== node.id) { return; } this.#gridAdornerActive = enabled; this.performUpdate(); }); } static animateOnDOMUpdate(treeElement: ElementsTreeElement): void { const tagName = treeElement.listItemElement.querySelector('.webkit-html-tag-name'); UI.UIUtils.runCSSAnimationOnce(tagName || treeElement.listItemElement, 'dom-update-highlight'); } static visibleShadowRoots(node: SDK.DOMModel.DOMNode): SDK.DOMModel.DOMNode[] { let roots = node.shadowRoots(); if (roots.length && !Common.Settings.Settings.instance().moduleSetting('show-ua-shadow-dom').get()) { roots = roots.filter(filter); } function filter(root: SDK.DOMModel.DOMNode): boolean { return root.shadowRootType() !== SDK.DOMModel.DOMNode.ShadowRootTypes.UserAgent; } return roots; } static canShowInlineText(node: SDK.DOMModel.DOMNode): boolean { if (node.contentDocument() || node.templateContent() || ElementsTreeElement.visibleShadowRoots(node).length || node.hasPseudoElements()) { return false; } if (node.nodeType() !== Node.ELEMENT_NODE) { return false; } if (!node.firstChild || node.firstChild !== node.lastChild || node.firstChild.nodeType() !== Node.TEXT_NODE) { return false; } const textChild = node.firstChild; const maxInlineTextChildLength = 80; if (textChild.nodeValue().length < maxInlineTextChildLength) { return true; } return false; } static populateForcedPseudoStateItems(contextMenu: UI.ContextMenu.ContextMenu, node: SDK.DOMModel.DOMNode): void { const pseudoClasses = ['active', 'hover', 'focus', 'visited', 'focus-within', 'focus-visible']; const forcedPseudoState = node.domModel().cssModel().pseudoState(node); const stateMenu = contextMenu.debugSection().appendSubMenuItem(i18nString(UIStrings.forceState), false, 'force-state'); for (const pseudoClass of pseudoClasses) { const pseudoClassForced = forcedPseudoState ? forcedPseudoState.indexOf(pseudoClass) >= 0 : false; stateMenu.defaultSection().appendCheckboxItem( ':' + pseudoClass, setPseudoStateCallback.bind(null, pseudoClass, !pseudoClassForced), {checked: pseudoClassForced, jslogContext: pseudoClass}); } function setPseudoStateCallback(pseudoState: string, enabled: boolean): void { node.domModel().cssModel().forcePseudoState(node, pseudoState, enabled); } } get adorners(): Adorners.Adorner.Adorner[] { return Array.from(this.#adorners); } performUpdate(): void { DEFAULT_VIEW( { containerAdornerActive: this.#containerAdornerActive, adorners: !this.isClosingTag() ? this.#adorners : undefined, showAdAdorner: this.nodeInternal.isAdFrameNode(), showContainerAdorner: Boolean(this.#layout?.isContainer) && !this.isClosingTag(), showFlexAdorner: Boolean(this.#layout?.isFlex) && !this.isClosingTag(), flexAdornerActive: this.#flexAdornerActive, showGridAdorner: Boolean(this.#layout?.isGrid) && !this.isClosingTag(), showGridLanesAdorner: Boolean(this.#layout?.isGridLanes) && !this.isClosingTag(), showMediaAdorner: this.node().isMediaNode() && !this.isClosingTag(), showPopoverAdorner: Boolean(Root.Runtime.hostConfig.devToolsAllowPopoverForcing?.enabled) && Boolean(this.node().attributes().find(attr => attr.name === 'popover')) && !this.isClosingTag(), showTopLayerAdorner: this.node().topLayerIndex() !== -1 && !this.isClosingTag(), gridAdornerActive: this.#gridAdornerActive, popoverAdornerActive: this.#popoverAdornerActive, isSubgrid: Boolean(this.#layout?.isSubgrid), nodeInfo: this.#nodeInfo, topLayerIndex: this.node().topLayerIndex(), onGutterClick: this.showContextMenu.bind(this), onAdornerAdded: adorner => { ElementsPanel.instance().registerAdorner(adorner); }, onAdornerRemoved: adorner => { ElementsPanel.instance().deregisterAdorner(adorner); }, onContainerAdornerClick: (event: Event) => this.#onContainerAdornerClick(event), onFlexAdornerClick: (event: Event) => this.#onFlexAdornerClick(event), onGridAdornerClick: (event: Event) => this.#onGridAdornerClick(event), onMediaAdornerClick: (event: Event) => this.#onMediaAdornerClick(event), onPopoverAdornerClick: (event: Event) => this.#onPopoverAdornerClick(event), onTopLayerAdornerClick: () => { if (!this.treeOutline) { return; } this.treeOutline.revealInTopLayer(this.node()); }, }, this, this.listItemElement); } #onContainerAdornerClick(event: Event): void { event.stopPropagation(); const node = this.node(); const nodeId = node.id; if (!nodeId) { return; } const model = node.domModel().overlayModel(); if (model.isHighlightedContainerQueryInPersistentOverlay(nodeId)) { model.hideContainerQueryInPersistentOverlay(nodeId); this.#containerAdornerActive = false; } else { model.highlightContainerQueryInPersistentOverlay(nodeId); this.#containerAdornerActive = true; Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED); } void this.updateAdorners(); } #onFlexAdornerClick(event: Event): void { event.stopPropagation(); const node = this.node(); const nodeId = node.id; if (!nodeId) { return; } const model = node.domModel().overlayModel(); if (model.isHighlightedFlexContainerInPersistentOverlay(nodeId)) { model.hideFlexContainerInPersistentOverlay(nodeId); this.#flexAdornerActive = false; } else { model.highlightFlexContainerInPersistentOverlay(nodeId); this.#flexAdornerActive = true; Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED); } void this.updateAdorners(); } #onGridAdornerClick(event: Event): void { event.stopPropagation(); const node = this.node(); const nodeId = node.id; if (!nodeId) { return; } const model = node.domModel().overlayModel(); if (model.isHighlightedGridInPersistentOverlay(nodeId)) { model.hideGridInPersistentOverlay(nodeId); this.#gridAdornerActive = false; } else { model.highlightGridInPersistentOverlay(nodeId); this.#gridAdornerActive = true; if (this.#layout?.isSubgrid) { Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED); } } void this.updateAdorners(); } async #onMediaAdornerClick(event: Event): Promise<void> { event.stopPropagation(); await UI.ViewManager.ViewManager.instance().showView('medias'); const view = UI.ViewManager.ViewManager.instance().view('medias'); if (view) { const widget = await view.widget(); if (widget instanceof Media.MainView.MainView) { await widget.waitForInitialPlayers(); widget.selectPlayerByDOMNodeId(this.node().backendNodeId()); } } } highlightAttribute(attributeName: string): void { // If the attribute is not found, we highlight the tag name instead. let animationElement = this.listItemElement.querySelector('.webkit-html-tag-name') ?? this.listItemElement; if (this.nodeInternal.getAttribute(attributeName) !== undefined) { const tag = this.listItemElement.getElementsByClassName('webkit-html-tag')[0]; const attributes = tag.getElementsByClassName('webkit-html-attribute'); for (const attribute of attributes) { const attributeElement = attribute.getElementsByClassName('webkit-html-attribute-name')[0]; if (attributeElement.textContent === attributeName) { animationElement = attributeElement; break; } } } UI.UIUtils.runCSSAnimationOnce(animationElement, 'dom-update-highlight'); } isClosingTag(): boolean { return !isOpeningTag(this.tagTypeContext); } node(): SDK.DOMModel.DOMNode { return this.nodeInternal; } isEditing(): boolean { return Boolean(this.editing); } highlightSearchResults(searchQuery: string): void { this.searchQuery = searchQuery; if (!this.editing) { this.#highlightSearchResults(); } } hideSearchHighlights(): void { Highlighting.HighlightManager.HighlightManager.instance().removeHighlights(this.#highlights); this.#highlights = []; } setInClipboard(inClipboard: boolean): void { if (this.inClipboard === inClipboard) { return; } this.inClipboard = inClipboard; this.listItemElement.classList.toggle('in-clipboard', inClipboard); } get hovered(): boolean { return this.#hovered; } set hovered(isHovered: boolean) { if (this.#hovered === isHovered) { return; } if (isHovered && !this.aiButtonContainer) { this.createAiButton(); } else if (!isHovered && this.aiButtonContainer) { this.aiButtonContainer.remove(); delete this.aiButtonContainer; } this.#hovered = isHovered; if (this.listItemElement) { if (isHovered) { this.createSelection(); this.listItemElement.classList.add('hovered'); } else { this.listItemElement.classList.remove('hovered'); } } } addIssue(newIssue: IssuesManager.Issue.Issue): void { if (this.#elementIssues.has(newIssue.primaryKey())) { return; } this.#elementIssues.set(newIssue.primaryKey(), newIssue); this.#applyIssueStyleAndTooltip(newIssue); } #applyIssueStyleAndTooltip(issue: IssuesManager.Issue.Issue): void { const elementIssueDetails = getElementIssueDetails(issue); if (!elementIssueDetails) { return; } if (elementIssueDetails.attribute) { this.#highlightViolatingAttr(elementIssueDetails.attribute, issue); } else { this.#highlightTagAsViolating(issue); } } get issuesByNodeElement(): Map<Element, IssuesManager.Issue.Issue[]> { return this.#nodeElementToIssue; } #highlightViolatingAttr(name: string, issue: IssuesManager.Issue.Issue): void { const tag = this.listItemElement.getElementsByClassName('webkit-html-tag')[0]; const attributes = tag.getElementsByClassName('webkit-html-attribute'); for (const attribute of attributes) { if (attribute.getElementsByClassName('webkit-html-attribute-name')[0].textContent === name) { const attributeElement = attribute.getElementsByClassName('webkit-html-attribute-name')[0]; attributeElement.classList.add('violating-element'); this.#updateNodeElementToIssue(attributeElement, issue); } } } #highlightTagAsViolating(issue: IssuesManager.Issue.Issue): void { const tagElement = this.listItemElement.getElementsByClassName('webkit-html-tag-name')[0]; tagElement.classList.add('violating-element'); this.#updateNodeElementToIssue(tagElement, issue); } #updateNodeElementToIssue(nodeElement: Element, issue: IssuesManager.Issue.Issue): void { let issues = this.#nodeElementToIssue.get(nodeElement); if (!issues) { issues = []; this.#nodeElementToIssue.set(nodeElement, issues); } issues.push(issue); this.treeOutline?.updateNodeElementToIssue(nodeElement, issues); } expandedChildrenLimit(): number { return this.#expandedChildrenLimit; } setExpandedChildrenLimit(expandedChildrenLimit: number): void { this.#expandedChildrenLimit = expandedChildrenLimit; } createSlotLink(nodeShortcut: SDK.DOMModel.DOMNodeShortcut|null): void { if (!isOpeningTag(this.tagTypeContext)) { return; } if (nodeShortcut) { const config = ElementsComponents.AdornerManager.getRegisteredAdorner( ElementsComponents.AdornerManager.RegisteredAdorners.SLOT); const adorner = this.adornSlot(config); this.#adorners.add(adorner); const deferredNode = nodeShortcut.deferredNode; adorner.addEventListener('click', () => { deferredNode.resolve(node => { void Common.Revealer.reveal(node); }); }); adorner.addEventListener('mousedown', e => e.consume(), false); } } private createSelection(): void { const contentElement = this.contentElement; if (!contentElement) { return; } if (!this.selectionElement) { this.selectionElement = document.createElement('div'); this.selectionElement.className = 'selection fill'; this.selectionElement.style.setProperty('margin-left', (-this.computeLeftIndent()) + 'px'); contentElement.prepend(this.selectionElement); } } private createHint(): void { if (this.contentElement && !this.hintElement) { this.hintElement = this.contentElement.createChild('span', 'selected-hint'); const selectedElementCommand = '$0'; UI.Tooltip.Tooltip.install( this.hintElement, i18nString(UIStrings.useSInTheConsoleToReferToThis, {PH1: selectedElementCommand})); UI.ARIAUtils.setHidden(this.hintElement, true); } } private createAiButton(): void { const isElementNode = this.node().nodeType() === Node.ELEMENT_NODE; if (!isElementNode || !UI.ActionRegistry.ActionRegistry.instance().hasAction('freestyler.elements-floating-button')) { return; } const action = UI.ActionRegistry.ActionRegistry.instance().getAction('freestyler.elements-floating-button'); if (this.contentElement && !this.aiButtonContainer) { this.aiButtonContainer = this.contentElement.createChild('span', 'ai-button-container'); const floatingButton = Buttons.FloatingButton.create('smart-assistant', action.title(), 'ask-ai'); floatingButton.addEventListener('click', ev => { ev.stopPropagation(); this.select(true, false); void action.execute(); }, {capture: true}); floatingButton.addEventListener('mousedown', ev => { ev.stopPropagation(); }, {capture: true}); this.aiButtonContainer.appendChild(floatingButton); } } override onbind(): void { if (this.treeOutline && !this.isClosingTag()) { this.treeOutline.treeElementByNode.set(this.nodeInternal, this); this.nodeInternal.addEventListener(SDK.DOMModel.DOMNodeEvents.TOP_LAYER_INDEX_CHANGED, this.performUpdate, this); } } override onunbind(): void { if (this.editing) { this.editing.cancel(); } if (this.treeOutline && this.treeOutline.treeElementByNode.get(this.nodeInternal) === this) { this.treeOutline.treeElementByNode.delete(this.nodeInternal); } this.nodeInternal.removeEventListener(SDK.DOMModel.DOMNodeEvents.TOP_LAYER_INDEX_CHANGED, this.performUpdate, this); } override onattach(): void { if (this.#hovered) { this.createSelection(); this.listItemElement.classList.add('hovered'); } this.updateTitle(); this.listItemElement.draggable = true; } override async onpopulate(): Promise<void> { if (this.treeOutline) { return await this.treeOutline.populateTreeElement(this); } } override async expandRecursively(): Promise<void> { await this.nodeInternal.getSubtree(100, true); await super.expandRecursively(Number.MAX_VALUE); } override onexpand(): void { if (this.isClosingTag()) { return; } this.updateTitle(); } override oncollapse(): void { if (this.isClosingTag()) { return; } this.updateTitle(); } override select(omitFocus?: boolean, selectedByUser?: boolean): boolean { if (this.editing) { return false; } const handledByFloaty = UI.Floaty.onFloatyClick({ type: UI.Floaty.FloatyContextTypes.ELEMENT_NODE_ID, data: {nodeId: this.nodeInternal.id}, }); if (handledByFloaty) { return false; } return super.select(omitFocus, selectedByUser); } override onselect(selectedByUser?: boolean): boolean { if (!this.treeOutline) { return false; } this.treeOutline.suppressRevealAndSelect = true; this.treeOutline.selectDOMNode(this.nodeInternal, selectedByUser); if (selectedByUser) { this.nodeInternal.highlight(); Host.userMetrics.actionTaken(Host.UserMetrics.Action.ChangeInspectedNodeInElementsPanel); } this.createSelection(); this.createHint(); this.treeOutline.suppressRevealAndSelect = false; return true; } override ondelete(): boolean { if (!this.treeOutline) { return false; } const startTagTreeElement = this.treeOutline.findTreeElement(this.nodeInternal); startTagTreeElement ? (void startTagTreeElement.remove()) : (void this.remove()); return true; } override onenter(): boolean { // On Enter or Return start editing the first attribute // or create a new attribute on the selected element. if (this.editing) { return false; } this.startEditing(); // prevent a newline from being immediately inserted return true; } override selectOnMouseDown(event: MouseEvent): void { super.selectOnMouseDown(event); if (this.editing) { return; } // Prevent selecting the nearest word on double click. if (event.detail >= 2) { event.preventDefault(); } } override ondblclick(event: Event): boolean { if (this.editing || this.isClosingTag()) { return false; } if (this.startEditingTarget((event.target as Element))) { return false; } if (this.isExpandable() && !this.expanded) { this.expand(); } return false; } hasEditableNode(): boolean { return !this.nodeInternal.isShadowRoot() && !this.nodeInternal.ancestorUserAgentShadowRoot(); } private insertInLastAttributePosition(tag: Element, node: Element): void { if (tag.getElementsByClassName('webkit-html-attribute').length > 0) { tag.insertBefore(node, tag.lastChild); } else if (tag.textContent !== null) { const matchResult = tag.textContent.match(/^<(.*?)>$/); if (!matchResult) { return; } const nodeName = matchResult[1]; tag.textContent = ''; UI.UIUtils.createTextChild(tag, '<' + nodeName); tag.appendChild(node); UI.UIUtils.createTextChild(tag, '>'); } } private startEditingTarget(eventTarget: Element): boolean { if (!this.treeOutline || this.treeOutline.selectedDOMNode() !== this.nodeInternal) { return false; } if (this.nodeInternal.nodeType() !== Node.ELEMENT_NODE && this.nodeInternal.nodeType() !== Node.TEXT_NODE) { return false; } const textNode = eventTarget.enclosingNodeOrSelfWithClass('webkit-html-text-node'); if (textNode) { return this.startEditingTextNode(textNode); } const attribute = eventTarget.enclosingNodeOrSelfWithClass('webkit-html-attribute'); if (attribute) { return this.startEditingAttribute(attribute, eventTarget); } const tagName = eventTarget.enclosingNodeOrSelfWithClass('webkit-html-tag-name'); if (tagName) { return this.startEditingTagName(tagName); } const newAttribute = eventTarget.enclosingNodeOrSelfWithClass('add-attribute'); if (newAttribute) { return this.addNewAttribute(); } return false; } private showContextMenu(event: Event): void { this.treeOutline && void this.treeOutline.showContextMenu(this, event); } async populateTagContextMenu(contextMenu: UI.ContextMenu.ContextMenu, event: Event): Promise<void> { // Add attribute-related actions. const treeElement = this.isClosingTag() && this.treeOutline ? this.treeOutline.findTreeElement(this.nodeInternal) : this; if (!treeElement) { return; } contextMenu.editSection().appendItem( i18nString(UIStrings.addAttribute), treeElement.addNewAttribute.bind(treeElement), {jslogContext: 'add-attribute'}); const target = (event.target as Element); const attribute = target.enclosingNodeOrSelfWithClass('webkit-html-attribute'); const newAttribute = target.enclosingNodeOrSelfWithClass('add-attribute'); if (attribute && !newAttribute) { contextMenu.editSection().appendItem( i18nString(UIStrings.editAttribute), this.startEditingAttribute.