| // 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); |
| } |
| } |
| } |