UNPKG

chrome-devtools-frontend

Version:
185 lines (163 loc) 6.21 kB
// Copyright 2025 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 */ /* eslint-disable @devtools/no-lit-render-outside-of-view */ import * as Annotations from '../../models/annotations/annotations.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; import {html, nothing, render} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import annotationStyles from './annotation.css.js'; // This class handles drawing of Annotations for the GreenDev project, but // is not for general use (at the moment). // // **Important**: all of this functionality is behind the GreenDev flag. We // have **no intention** of pushing this feature live in this state. This // is code landing to user test in Canary that will not ship without an // additional project to make this code fully production worthy. That is // why this CL has no tests, for example. // The label is angled on the left from the centre of the entry it belongs to. // `LABEL_AND_CONNECTOR_SHIFT_LENGTH` specifies how many pixels to the left it is shifted. const LABEL_AND_CONNECTOR_SHIFT_LENGTH = 8; // Length of the line that connects the label to the entry. const LABEL_CONNECTOR_HEIGHT = 7; interface ViewInput { inputText: string; isExpanded: boolean; anchored: boolean; expandable: boolean; showCloseButton: boolean; clickHandler: () => void; closeHandler: () => void; } type View = (input: ViewInput, output: undefined, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, _, target) => { const {inputText: label, isExpanded, anchored, expandable, showCloseButton, clickHandler, closeHandler} = input; // TODO(finnur): Use `x`, and `y` passed via `input` to set the coordinates for the // *Widget* (not the `overlay` div), then remove the `this.element.style` calls and // remove the lint override no-imperative-dom-api from the top. const connectorColor = ThemeSupport.ThemeSupport.instance().getComputedValue('--color-text-primary'); const overlayStyles = [ anchored ? 'left: 17px; top: 11px;' : '', !expandable ? 'pointer-events: none;' : '', ].join(' '); // clang-format off render(html` <style>${annotationStyles}</style> ${anchored ? html` <svg class="connectorContainer" width=${LABEL_AND_CONNECTOR_SHIFT_LENGTH * 2} height=${LABEL_CONNECTOR_HEIGHT}> <line x1=${LABEL_AND_CONNECTOR_SHIFT_LENGTH} y1=0 x2=${LABEL_AND_CONNECTOR_SHIFT_LENGTH * 2} y2=${LABEL_CONNECTOR_HEIGHT} stroke=${connectorColor} stroke-width=2 /> <circle cx=${LABEL_AND_CONNECTOR_SHIFT_LENGTH} cy=0 r=3 fill=${connectorColor} /> </svg> ` : nothing} <div class='overlay' style=${overlayStyles} @click=${expandable ? clickHandler : null}> ${isExpanded ? label : '!'} </div> ${showCloseButton ? html`<svg @click=${closeHandler} class="close-button" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="8" cy="8" r="7.5" fill="#EEE" stroke="#888"/> <path d="M5 5L11 11M5 11L11 5" stroke="#888" stroke-width="2"/> </svg>` : nothing} `, target); // clang-format on }; export class Annotation extends UI.Widget.Widget { readonly #view: View; readonly #id: number; #inputText: string; #x = 0; #y = 0; #isExpanded = false; #hasShown = false; #anchored = false; #expandable = false; #showCloseButton = false; constructor( id: number, label: string, showExpanded: boolean, anchored: boolean, expandable: boolean, showCloseButton: boolean, view = DEFAULT_VIEW) { super({jslog: `${VisualLogging.panel('annotation').track({resize: true})}`, useShadowDom: true}); this.#id = id; this.#view = view; this.#isExpanded = showExpanded; this.#inputText = label; this.#anchored = anchored; this.#expandable = expandable; this.#showCloseButton = showCloseButton; } #toggle(): void { this.#isExpanded = !this.#isExpanded; this.requestUpdate(); } #closeHandler(): void { this.hide(); Annotations.AnnotationRepository.instance().deleteAnnotation(this.#id); } override wasShown(): void { this.element.style.position = 'absolute'; this.element.style.left = `${this.#x}px`; this.element.style.top = `${this.#y}px`; super.wasShown(); this.#hasShown = true; this.requestUpdate(); } override performUpdate(): void { if (!this.isShowing()) { return; } const input = { inputText: this.#inputText, isExpanded: this.#isExpanded, anchored: this.#anchored, expandable: this.#expandable, showCloseButton: this.#showCloseButton, x: this.#x, y: this.#y, clickHandler: this.#toggle.bind(this), closeHandler: this.#closeHandler.bind(this), }; this.#view(input, undefined, this.contentElement); if (this.#showCloseButton) { const overlay = this.contentElement.querySelector('.overlay') as HTMLElement | null; const closeButton = this.contentElement.querySelector('.close-button') as HTMLElement | null; if (overlay && closeButton) { const overlayLeft = parseFloat(overlay.style.left || '0'); const overlayWidth = overlay.getBoundingClientRect().width; // Position the button to the right of the overlay, adjusting for button width. closeButton.style.left = `${overlayLeft + overlayWidth - 16}px`; } } } hide(): void { this.detach(); } getCoordinates(): {x: number, y: number} { return {x: this.#x, y: this.#y}; } setCoordinates(x: number, y: number): void { this.#x = x; this.#y = y; if (this.isShowing()) { this.element.style.left = `${this.#x}px`; this.element.style.top = `${this.#y}px`; } this.requestUpdate(); } hasShown(): boolean { return this.#hasShown; } }