UNPKG

chrome-devtools-frontend

Version:
1,222 lines (1,134 loc) 116 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 '../../ui/components/adorners/adorners.js'; 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 Workspace from '../../models/workspace/workspace.js'; import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js'; import type {DirectiveResult} from '../../third_party/lit/lib/directive.js'; import type * 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 {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}} = 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 Label of an adorner next to the html node in the Elements panel.    */ viewSourceCode: 'View source code', /** * @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; containerType?: string; showFlexAdorner: boolean; showGridAdorner: boolean; showGridLanesAdorner: boolean; showMediaAdorner: boolean; showPopoverAdorner: boolean; showTopLayerAdorner: boolean; isSubgrid: boolean; showViewSourceAdorner: boolean; showScrollAdorner: boolean; showScrollSnapAdorner: boolean; nodeInfo?: DocumentFragment; topLayerIndex: number; scrollSnapAdornerActive: boolean; onGutterClick: (e: Event) => void; onContainerAdornerClick: (e: Event) => void; onFlexAdornerClick: (e: Event) => void; onGridAdornerClick: (e: Event) => void; onMediaAdornerClick: (e: Event) => void; onPopoverAdornerClick: (e: Event) => void; onScrollSnapAdornerClick: (e: Event) => void; onTopLayerAdornerClick: (e: Event) => void; onViewSourceAdornerClick: () => void; onSlotAdornerClick: (e: Event) => void; showSlotAdorner: boolean; slotName?: string; showStartingStyleAdorner: boolean; startingStyleAdornerActive: boolean; onStartingStyleAdornerClick: (e: Event) => void; } export interface ViewOutput { gutterContainer?: HTMLElement; decorationsElement?: HTMLElement; contentElement?: HTMLElement; } export function adornerRef(): DirectiveResult<typeof Lit.Directives.RefDirective> { let adorner: Adorners.Adorner.Adorner|undefined; return ref((el?: Element) => { if (adorner) { ElementsPanel.instance().deregisterAdorner(adorner); } adorner = el as Adorners.Adorner.Adorner; if (adorner) { if (ElementsPanel.instance().isAdornerEnabled(adorner.name)) { adorner.show(); } else { adorner.hide(); } ElementsPanel.instance().registerAdorner(adorner); } }); } function handleAdornerKeydown(cb: (event: Event) => void): (event: KeyboardEvent) => void { return (event: KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'Space') { cb(event); event.preventDefault(); event.stopPropagation(); } }; } export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLElement): void => { const hasAdorners = input.showAdAdorner || input.showContainerAdorner || input.showFlexAdorner || input.showGridAdorner || input.showGridLanesAdorner || input.showMediaAdorner || input.showPopoverAdorner || input.showTopLayerAdorner || input.showViewSourceAdorner || input.showScrollAdorner || input.showScrollSnapAdorner || input.showSlotAdorner || input.showStartingStyleAdorner; // 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)} .name=${ElementsComponents.AdornerManager.RegisteredAdorners.AD} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.AD)} ${adornerRef()}> <span>${ElementsComponents.AdornerManager.RegisteredAdorners.AD}</span> </devtools-adorner>` : nothing} ${input.showViewSourceAdorner ? html`<devtools-adorner .name=${ElementsComponents.AdornerManager.RegisteredAdorners.VIEW_SOURCE} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.VIEW_SOURCE)} aria-label=${i18nString(UIStrings.viewSourceCode)} @click=${input.onViewSourceAdornerClick} ${adornerRef()}> <span>${ElementsComponents.AdornerManager.RegisteredAdorners.VIEW_SOURCE}</span> </devtools-adorner>` : nothing} ${input.showContainerAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .name=${ElementsComponents.AdornerManager.RegisteredAdorners.CONTAINER} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.CONTAINER).track({click: true})} active=${input.containerAdornerActive} aria-label=${input.containerAdornerActive ? i18nString(UIStrings.enableContainer) : i18nString(UIStrings.disableContainer)} @click=${input.onContainerAdornerClick} @keydown=${handleAdornerKeydown(input.onContainerAdornerClick)} ${adornerRef()}> <span class="adorner-with-icon"> <devtools-icon name="container"></devtools-icon> <span>${input.containerType}</span> </span> </devtools-adorner>`: nothing} ${input.showFlexAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .name=${ElementsComponents.AdornerManager.RegisteredAdorners.FLEX} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.FLEX).track({click: true})} active=${input.flexAdornerActive} aria-label=${input.flexAdornerActive ? i18nString(UIStrings.disableFlexMode) : i18nString(UIStrings.enableFlexMode)} @click=${input.onFlexAdornerClick} @keydown=${handleAdornerKeydown(input.onFlexAdornerClick)} ${adornerRef()}> <span>${ElementsComponents.AdornerManager.RegisteredAdorners.FLEX}</span> </devtools-adorner>`: nothing} ${input.showGridAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .name=${input.isSubgrid ? ElementsComponents.AdornerManager.RegisteredAdorners.SUBGRID : ElementsComponents.AdornerManager.RegisteredAdorners.GRID} jslog=${VisualLogging.adorner(input.isSubgrid ? ElementsComponents.AdornerManager.RegisteredAdorners.SUBGRID : ElementsComponents.AdornerManager.RegisteredAdorners.GRID).track({click: true})} active=${input.gridAdornerActive} aria-label=${input.gridAdornerActive ? i18nString(UIStrings.disableGridMode) : i18nString(UIStrings.enableGridMode)} @click=${input.onGridAdornerClick} @keydown=${handleAdornerKeydown(input.onGridAdornerClick)} ${adornerRef()}> <span>${input.isSubgrid ? ElementsComponents.AdornerManager.RegisteredAdorners.SUBGRID : ElementsComponents.AdornerManager.RegisteredAdorners.GRID}</span> </devtools-adorner>`: nothing} ${input.showGridLanesAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .name=${ElementsComponents.AdornerManager.RegisteredAdorners.GRID_LANES} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.GRID_LANES).track({click: true})} active=${input.gridAdornerActive} aria-label=${input.gridAdornerActive ? i18nString(UIStrings.disableGridLanesMode) : i18nString(UIStrings.enableGridLanesMode)} @click=${input.onGridAdornerClick} @keydown=${handleAdornerKeydown(input.onGridAdornerClick)} ${adornerRef()}> <span>${ElementsComponents.AdornerManager.RegisteredAdorners.GRID_LANES}</span> </devtools-adorner>`: nothing} ${input.showMediaAdorner ? html`<devtools-adorner class=clickable role=button tabindex=0 .name=${ElementsComponents.AdornerManager.RegisteredAdorners.MEDIA} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.MEDIA).track({click: true})} aria-label=${i18nString(UIStrings.openMediaPanel)} @click=${input.onMediaAdornerClick} @keydown=${handleAdornerKeydown(input.onMediaAdornerClick)} ${adornerRef()}> <span class="adorner-with-icon"> ${ElementsComponents.AdornerManager.RegisteredAdorners.MEDIA}<devtools-icon name="select-element"></devtools-icon> </span> </devtools-adorner>`: nothing} ${input.showPopoverAdorner ? html`<devtools-adorner class=clickable role=button toggleable=true tabindex=0 .name=${ElementsComponents.AdornerManager.RegisteredAdorners.POPOVER} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.POPOVER).track({click: true})} active=${input.popoverAdornerActive} aria-label=${input.popoverAdornerActive ? i18nString(UIStrings.stopForceOpenPopover) : i18nString(UIStrings.forceOpenPopover)} @click=${input.onPopoverAdornerClick} @keydown=${handleAdornerKeydown(input.onPopoverAdornerClick)} ${adornerRef()}> <span>${ElementsComponents.AdornerManager.RegisteredAdorners.POPOVER}</span> </devtools-adorner>`: nothing} ${input.showTopLayerAdorner ? html`<devtools-adorner class=clickable role=button tabindex=0 .name=${ElementsComponents.AdornerManager.RegisteredAdorners.TOP_LAYER} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.TOP_LAYER).track({click: true})} aria-label=${i18nString(UIStrings.reveal)} @click=${input.onTopLayerAdornerClick} @keydown=${handleAdornerKeydown(input.onTopLayerAdornerClick)} ${adornerRef()}> <span class="adorner-with-icon"> ${`top-layer (${input.topLayerIndex})`}<devtools-icon name="select-element"></devtools-icon> </span> </devtools-adorner>`: nothing} ${input.showStartingStyleAdorner ? html`<devtools-adorner class="starting-style" .name=${ElementsComponents.AdornerManager.RegisteredAdorners.STARTING_STYLE} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.STARTING_STYLE).track({click: true})} active=${input.startingStyleAdornerActive} toggleable=true aria-label=${input.startingStyleAdornerActive ? i18nString(UIStrings.disableStartingStyle) : i18nString(UIStrings.enableStartingStyle)} @click=${input.onStartingStyleAdornerClick} @keydown=${handleAdornerKeydown(input.onStartingStyleAdornerClick)} ${adornerRef()}> <span>${ElementsComponents.AdornerManager.RegisteredAdorners.STARTING_STYLE}</span> </devtools-adorner>` : nothing} ${input.showScrollAdorner ? html`<devtools-adorner class="scroll" .name=${ElementsComponents.AdornerManager.RegisteredAdorners.SCROLL} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.SCROLL).track({click: true})} aria-label=${i18nString(UIStrings.elementHasScrollableOverflow)} ${adornerRef()}> <span>${ElementsComponents.AdornerManager.RegisteredAdorners.SCROLL}</span> </devtools-adorner>` : nothing} ${input.showSlotAdorner ? html`<devtools-adorner class=clickable role=button tabindex=0 .name=${ElementsComponents.AdornerManager.RegisteredAdorners.SLOT} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.SLOT).track({click: true})} @click=${input.onSlotAdornerClick} @mousedown=${(e: Event) => e.stopPropagation()} ${adornerRef()}> <span class="adorner-with-icon"> <devtools-icon name="select-element"></devtools-icon> <span>${ElementsComponents.AdornerManager.RegisteredAdorners.SLOT}</span> </span> </devtools-adorner>`: nothing} ${input.showScrollSnapAdorner ? html`<devtools-adorner class="scroll-snap" .name=${ElementsComponents.AdornerManager.RegisteredAdorners.SCROLL_SNAP} jslog=${VisualLogging.adorner(ElementsComponents.AdornerManager.RegisteredAdorners.SCROLL_SNAP).track({click: true})} active=${input.scrollSnapAdornerActive} toggleable=true aria-label=${input.scrollSnapAdornerActive ? i18nString(UIStrings.disableScrollSnap) : i18nString(UIStrings.enableScrollSnap)} @click=${input.onScrollSnapAdornerClick} @keydown=${handleAdornerKeydown(input.onScrollSnapAdornerClick)} ${adornerRef()}> <span>${ElementsComponents.AdornerManager.RegisteredAdorners.SCROLL_SNAP}</span> </devtools-adorner>` : nothing} </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); #nodeInfo?: DocumentFragment; #containerAdornerActive = false; #flexAdornerActive = false; #gridAdornerActive = false; #popoverAdornerActive = false; #scrollSnapAdornerActive = false; #startingStyleAdornerActive = 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.#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'); } } 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); } } performUpdate(): void { DEFAULT_VIEW( { containerAdornerActive: this.#containerAdornerActive, showAdAdorner: this.nodeInternal.isAdFrameNode(), showContainerAdorner: Boolean(this.#layout?.containerType) && !this.isClosingTag(), containerType: this.#layout?.containerType, 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), showViewSourceAdorner: this.nodeInternal.isRootNode() && isOpeningTag(this.tagTypeContext), showScrollAdorner: ((this.node().nodeName() === 'HTML' && this.node().ownerDocument?.isScrollable()) || (this.node().nodeName() !== '#document' && this.node().isScrollable())) && !this.isClosingTag(), showScrollSnapAdorner: Boolean(this.#layout?.hasScroll) && !this.isClosingTag(), scrollSnapAdornerActive: this.#scrollSnapAdornerActive, showSlotAdorner: Boolean(this.nodeInternal.assignedSlot) && !this.isClosingTag(), showStartingStyleAdorner: this.nodeInternal.affectedByStartingStyles() && !this.isClosingTag(), startingStyleAdornerActive: this.#startingStyleAdornerActive, nodeInfo: this.#nodeInfo, onStartingStyleAdornerClick: (event: Event) => this.#onStartingStyleAdornerClick(event), onSlotAdornerClick: () => { if (this.nodeInternal.assignedSlot) { const deferredNode = this.nodeInternal.assignedSlot.deferredNode; deferredNode.resolve(node => { void Common.Revealer.reveal(node); }); } }, topLayerIndex: this.node().topLayerIndex(), onViewSourceAdornerClick: this.revealHTMLInSources.bind(this), onGutterClick: this.showContextMenu.bind(this), 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), onScrollSnapAdornerClick: (event: Event) => this.#onScrollSnapAdornerClick(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; } 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); this.nodeInternal.addEventListener( SDK.DOMModel.DOMNodeEvents.SCROLLABLE_FLAG_UPDATED, this.#onScrollableFlagUpdated, this); this.nodeInternal.addEventListener( SDK.DOMModel.DOMNodeEvents.CONTAINER_QUERY_OVERLAY_STATE_CHANGED, this.#onPersistentContainerQueryOverlayStateChanged, this); this.nodeInternal.addEventListener( SDK.DOMModel.DOMNodeEvents.FLEX_CONTAINER_OVERLAY_STATE_CHANGED, this.#onPersistentFlexContainerOverlayStateChanged, this); this.nodeInternal.addEventListener( SDK.DOMModel.DOMNodeEvents.GRID_OVERLAY_STATE_CHANGED, this.#onPersistentGridOverlayStateChanged, this); this.nodeInternal.addEventListener( SDK.DOMModel.DOMNodeEvents.SCROLL_SNAP_OVERLAY_STATE_CHANGED, this.#onPersistentScrollSnapOverlayStateChanged, 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); this.nodeInternal.removeEventListener( SDK.DOMModel.DOMNodeEvents.SCROLLABLE_FLAG_UPDATED, this.#onScrollableFlagUpdated, this); this.nodeInternal.removeEventListener( SDK.DOMModel.DOMNodeEvents.CONTAINER_QUERY_OVERLAY_STATE_CHANGED, this.#onPersistentContainerQueryOverlayStateChanged, this); this.nodeInternal.removeEventListener( SDK.DOMModel.DOMNodeEvents.FLEX_CONTAINER_OVERLAY_STATE_CHANGED, this.#onPersistentFlexContainerOverlayStateChanged, this); this.nodeInternal.removeEventListener( SDK.DOMModel.DOMNodeEvents.GRID_OVERLAY_STATE_CHANGED, this.#onPersistentGridOverlayStateChanged, this); this.nodeInternal.removeEventListener( SDK.DOMModel.DOMNodeEvents.SCROLL_SNAP_OVERLAY_STATE_CHANGED, this.#onPersistentScrollSnapOverlayStateChanged, this); } #onScrollableFlagUpdated(): void { void this.#updateAdorners(); } #onPersistentContainerQueryOverlayStateChanged(event: Common.EventTarget.EventTargetEvent<{enabled: boolean}>): void { this.#containerAdornerActive = event.data.enabled; this.performUpdate(); } #onPersistentFlexContainerOverlayStateChanged(event: Common.EventTarget.EventTargetEvent<{enabled: boolean}>): void { this.#flexAdornerActive = event.data.enabled; this.performUpdate(); } #onPersistentGridOverlayStateChanged(event: Common.EventTarget.EventTargetEvent<{enabled: boolean}>): void { this.#gridAdornerActive = event.data.enabled; this.performUpdate(); } #onPersistentScrollSnapOverlayStateChanged(event: Common.EventTarget.EventTargetEvent<{enabled: boolean}>): void { this.#scrollSnapAdornerActive = event.data.enabled; this.performUpdate(); } #onScrollSnapAdornerClick(event: Event): void { event.stopPropagation(); const node = this.node(); const nodeId = node.id; if (!nodeId) { return; } const model = node.domModel().overlayModel(); if (this.#scrollSnapAdornerActive) { model.hideScrollSnapInPersistentOverlay(nodeId); } else { model.highlightScrollSnapInPersistentOverlay(nodeId); } } 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.sel