chrome-devtools-frontend
Version:
Chrome DevTools UI
185 lines (163 loc) • 6.21 kB
text/typescript
// 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} =${expandable ? clickHandler : null}>
${isExpanded ? label : '!'}
</div>
${showCloseButton ?
html`<svg =${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;
}
}