chrome-devtools-frontend
Version:
Chrome DevTools UI
209 lines (182 loc) • 8.52 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.
import type * as Common from '../../core/common/common.js';
import type * as SDK from '../../core/sdk/sdk.js';
import * as Annotations from '../../models/annotations/annotations.js';
import {Annotation} from './Annotation.js';
interface AnnotationData {
id: number;
type: Annotations.AnnotationType;
annotation: Annotation;
}
interface AnnotationPlacement {
parentElement: Element;
insertBefore?: Node|null;
resolveInitialState:
(parentElement: Element, reveal: boolean, lookupId: string,
anchor?: SDK.DOMModel.DOMNode|SDK.NetworkRequest.NetworkRequest) => Promise<{x: number, y: number}|null>;
}
// This class handles general management of Annotations, the data needed to display them and any panel-specific things
// that the AnnotationRepository must be free from. It is created on-demand the first time a panel, that wants to show
// an Annotation, appears.
//
// NOTE: For now this class is not for general use and is inactive (unless a specific flag is supplied).
export class AnnotationManager {
static #instance: AnnotationManager|null = null;
#annotationPlacements: Map<Annotations.AnnotationType, AnnotationPlacement>|null = null;
#annotations = new Map<number, AnnotationData>();
#synced = false;
constructor() {
if (!Annotations.AnnotationRepository.annotationsEnabled()) {
console.warn('AnnotationManager created with annotations disabled');
return;
}
Annotations.AnnotationRepository.instance().addEventListener(
Annotations.Events.ANNOTATION_ADDED, this.#onAnnotationAdded, this);
Annotations.AnnotationRepository.instance().addEventListener(
Annotations.Events.ANNOTATION_DELETED, this.#onAnnotationDeleted, this);
Annotations.AnnotationRepository.instance().addEventListener(
Annotations.Events.ALL_ANNOTATIONS_DELETED, this.#onAllAnnotationsDeleted, this);
}
static instance(): AnnotationManager {
if (!AnnotationManager.#instance) {
AnnotationManager.#instance = new AnnotationManager();
}
return AnnotationManager.#instance;
}
initializePlacementForAnnotationType(
type: Annotations.AnnotationType,
resolveInitialState:
(parentElement: Element, reveal: boolean, lookupId: string,
anchor?: SDK.DOMModel.DOMNode|SDK.NetworkRequest.NetworkRequest) => Promise<{x: number, y: number}|null>,
parentElement: Element, insertBefore: Node|null = null): void {
if (!Annotations.AnnotationRepository.annotationsEnabled()) {
return;
}
if (!this.#annotationPlacements) {
this.#annotationPlacements = new Map();
}
this.#annotationPlacements.set(type, {parentElement, insertBefore, resolveInitialState});
// eslint-disable-next-line no-console
console.log(
`[AnnotationManager] initializing placement for ${Annotations.AnnotationType[type]}`, {parentElement},
'placement count:', this.#annotationPlacements);
this.#syncAnnotations();
}
#syncAnnotations(): void {
if (this.#synced) {
return;
}
// eslint-disable-next-line no-console
console.log('[AnnotationManager] **** SYNC STARTED ***');
const repository = Annotations.AnnotationRepository.instance();
for (const type of Object.values(Annotations.AnnotationType)) {
for (const annotation of repository.getAnnotationDataByType(type as Annotations.AnnotationType)) {
// eslint-disable-next-line no-console
console.log(
'[AnnotationManager] Available annotation:', annotation,
'need sync:', !this.#annotations.has(annotation.id));
if (!this.#annotations.has(annotation.id)) {
this.#addAnnotation(annotation);
}
}
}
this.#synced = true;
}
#onAllAnnotationsDeleted(): void {
for (const annotation of this.#annotations.values()) {
annotation.annotation.hide();
}
this.#annotations = new Map();
// eslint-disable-next-line no-console
console.log('[AnnotationManager] deleted all annotations');
}
#onAnnotationDeleted(
event: Common.EventTarget.EventTargetEvent<Annotations.EventTypes[Annotations.Events.ANNOTATION_DELETED]>): void {
const {id} = event.data;
const annotation = this.#annotations.get(id);
if (annotation) {
annotation.annotation.hide();
this.#annotations.delete(id);
}
// eslint-disable-next-line no-console
console.log(`[AnnotationManager] Deleted annotation with id ${id}`);
}
#onAnnotationAdded(
event: Common.EventTarget.EventTargetEvent<Annotations.EventTypes[Annotations.Events.ANNOTATION_ADDED]>): void {
const annotationData = event.data;
// eslint-disable-next-line no-console
console.log('[AnnotationManager] handleAddAnnotation', annotationData);
this.#addAnnotation(annotationData);
}
#addAnnotation(annotationData: Annotations.BaseAnnotationData): void {
const expandable = annotationData.type !== Annotations.AnnotationType.NETWORK_REQUEST;
const showExpanded = annotationData.type !== Annotations.AnnotationType.NETWORK_REQUEST;
const showAnchored = annotationData.type !== Annotations.AnnotationType.NETWORK_REQUEST;
const showCloseButton = annotationData.type !== Annotations.AnnotationType.NETWORK_REQUEST;
const annotation = new Annotation(
annotationData.id, annotationData.message, showExpanded, showAnchored, expandable, showCloseButton);
this.#annotations.set(annotationData.id, {id: annotationData.id, type: annotationData.type, annotation});
// eslint-disable-next-line no-console
console.log('[AnnotationManager] addAnnotation called. Annotations now', this.#annotations);
requestAnimationFrame(async () => {
await this.#resolveAnnotationWithId(annotationData.id);
});
}
async resolveAnnotationsOfType(type: Annotations.AnnotationType): Promise<void> {
for (const annotationData of this.#annotations.values()) {
if (annotationData.type === type) {
await this.#resolveAnnotationWithId(annotationData.id);
}
}
}
async #resolveAnnotationWithId(id: number): Promise<void> {
const annotation = this.#annotations.get(id);
if (!annotation) {
console.warn('Unable to find annotation with id', id, ' in annotations map', this.#annotations);
return;
}
const placement = this.#annotationPlacements?.get(annotation.type);
if (!placement) {
console.warn(
'Unable to find placement for annotation with id', id,
'(note: this is expected if its panel hasn\'t been shown yet).');
return;
}
let position = undefined;
const annotationRegistration = Annotations.AnnotationRepository.instance().getAnnotationDataById(id);
const reveal = !annotation.annotation.hasShown();
switch (annotationRegistration?.type) {
case Annotations.AnnotationType.ELEMENT_NODE: {
const elementData = annotationRegistration as Annotations.ElementsAnnotationData;
position = await placement.resolveInitialState(
placement.parentElement, reveal, elementData.lookupId, elementData.anchor);
break;
}
case Annotations.AnnotationType.NETWORK_REQUEST: {
const networkRequestData = annotationRegistration as Annotations.NetworkRequestAnnotationData;
position = await placement.resolveInitialState(
placement.parentElement, reveal, networkRequestData.lookupId, networkRequestData.anchor);
break;
}
case Annotations.AnnotationType.NETWORK_REQUEST_SUBPANEL_HEADERS: {
const networkRequestDetailsData = annotationRegistration as Annotations.NetworkRequestDetailsAnnotationData;
position = await placement.resolveInitialState(
placement.parentElement, reveal, networkRequestDetailsData.lookupId, networkRequestDetailsData.anchor);
break;
}
default:
console.warn('[AnnotationManager] Unknown AnnotationType', annotationRegistration?.type);
}
if (!position) {
// eslint-disable-next-line no-console
console.log(`Unable to calculate position for annotation with id ${annotationRegistration?.id}`);
return;
}
annotation.annotation.setCoordinates(position.x, position.y);
if (!annotation.annotation.isShowing()) {
annotation.annotation.show(placement.parentElement, placement.insertBefore);
}
}
}